diff --git a/packages/node-cli/src/ApplicationConstants.js b/packages/node-cli/src/ApplicationConstants.js
index b58d353c..9ae32ef5 100644
--- a/packages/node-cli/src/ApplicationConstants.js
+++ b/packages/node-cli/src/ApplicationConstants.js
@@ -68,5 +68,11 @@ module.exports = {
SUITECLOUD_CI: 'SUITECLOUD_CI',
SUITECLOUD_CI_PASSKEY: 'SUITECLOUD_CI_PASSKEY',
SUITECLOUD_FALLBACK_PASSKEY: 'SUITECLOUD_FALLBACK_PASSKEY'
- }
+ },
+ HTTP_RESPONSE_CODE: {
+ UNAUTHORIZED: 401,
+ FORBIDDEN: 403,
+ INTERNAL_SERVER_ERROR: 500,
+ SERVICE_UNAVAILABLE: 503,
+ },
};
diff --git a/packages/node-cli/src/services/SuiteCloudAuthProxyService.js b/packages/node-cli/src/services/SuiteCloudAuthProxyService.js
index 533afb93..1dfb7a0b 100644
--- a/packages/node-cli/src/services/SuiteCloudAuthProxyService.js
+++ b/packages/node-cli/src/services/SuiteCloudAuthProxyService.js
@@ -25,6 +25,7 @@ const {
} = require('../utils/AuthenticationUtils');
const {
AUTHORIZATION_PROPERTIES_KEYS,
+ HTTP_RESPONSE_CODE,
} = require('../ApplicationConstants');
/** Message literal service method */
@@ -39,14 +40,6 @@ const LOCAL_HOSTNAME = '127.0.0.1';
/** Target server port */
const TARGET_SERVER_PORT = 443;
-/** Http codes */
-const HTTP_RESPONSE_CODE = {
- UNAUTHORIZED: 401,
- FORBIDDEN: 403,
- INTERNAL_SERVER_ERROR: 500,
- SERVICE_UNAVAILABLE: 503,
-};
-
class SuiteCloudAuthProxyService extends EventEmitter {
constructor(sdkPath, executionEnvironmentContext) {
super();
diff --git a/packages/vscode-extension/messages.json b/packages/vscode-extension/messages.json
index 64c0fc5a..39c9c433 100644
--- a/packages/vscode-extension/messages.json
+++ b/packages/vscode-extension/messages.json
@@ -10,9 +10,12 @@
"ANSWERS_NO": "No",
"ANSWERS_YES": "Yes",
+ "BUTTONS_DONT_SHOW_AGAIN": "Don't show again",
+ "BUTTONS_GIVE_FEEDBACK": "Give Feedback",
+ "BUTTONS_OK": "Ok",
+ "BUTTONS_OPEN_SETTINGS": "Open Settings",
"BUTTONS_RESTART_NOW": "Restart Now",
"BUTTONS_SEE_DETAILS": "See Details",
- "BUTTONS_OK": "Ok",
"COMMAND_ERROR": "Something went wrong with '{0}'.",
"COMMAND_SUCCESS": "'{0}' has been successfully executed.",
@@ -54,6 +57,17 @@
"DEPLOY_QUESTIONS_CHOICES_ACCOUNT_SPECIFIC_VALUES_CANCEL_PROCESS": "Cancel the deployment process",
"DEPLOY_QUESTIONS_CHOICES_ACCOUNT_SPECIFIC_VALUES_DISPLAY_WARNING": "Display a warning and continue the deployment process",
+ "DEVASSIST_SERVICE_FEEDBACK_FORM_GENERIC_VALIDATION_ERROR_WRAPPER": "Some fields have validation errors.
{0}",
+ "DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_VALIDATION_ERROR": "{0}: {1}",
+ "DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_CANNOT_BE_EMPTY_TEXT": "This field cannot not be empty.",
+ "DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_CANNOT_BE_EMPTY_MULTIPLE_OPTION": "At least one of the available options must be selected.",
+ "DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_CANNOT_BE_EMPTY_NUMERIC": "Choose a satisfaction level (for example, 3 out of 5 stars).",
+ "DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_CANNOT_BE_TOO_LONG": "This field can only contain up to {0} characters.",
+ "DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_CANNOT_HAVE_REPEATED_VALUES": "Select unique values (provided: {0}).",
+ "DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_MUST_HAVE_BE_A_VALID_NUMBER": "Choose a natural number between {0} and {1}.",
+ "DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_MUST_HAVE_SPECIFIC_VALUES": "Select any values from the ones available: {0}.",
+ "DEVASSIST_SERVICE_FEEDBACK_FORM_SUBMITTING_ERROR": "Unable to connect to the SuiteCloud Developer Assistant service. Is it running?",
+
"DEVASSIST_SERVICE_OUTPUT_PROXY_ERROR": "There was a problem while running SuiteCloud Developer Assistant service.\n{0}",
"DEVASSIST_SERVICE_OUTPUT_SERVER_ERROR": "There was a server error while running SuiteCloud Developer Assistant service.\n{0}",
"DEVASSIST_SERVICE_OUTPUT_SERVER_ERROR_ON_REFRESH": "There was a problem refreshing credentials while running SuiteCloud Developer Assistant service.\n{0}",
@@ -66,13 +80,9 @@
"DEVASSIST_SERVICE_IS_STOPPED_OUTPUT": "There was a problem when starting SuiteCloud Developer Assistant service.\n{0}",
"DEVASSIST_SERVICE_IS_STOPPED_STATUSBAR": "Dev Assist: stopped",
"DEVASSIST_SERVICE_SERVER_ERROR_OUTPUT": "A server error has occurred while running SuiteCloud Developer Assistant service.\nError: {0}",
- "DEVASSIST_SERVICE_STARTUP_BUTTON_DONT_SHOW_AGAIN": "Don't show again",
- "DEVASSIST_SERVICE_STARTUP_BUTTON_OPEN_SETTINGS": "Open Settings",
- "DEVASSIST_SERVICE_SEE_DETAILS_BUTTON": "See Details",
"DEVASSIST_SERVICE_STARTUP_MESSAGE": "SuiteCloud Developer Assistant is here. Open settings to start using it.",
"DEVASSIST_SERVICE_STATUSBAR_TOOLTIP": "Shows the status of SuiteCloud Developer Assistant service.",
-
"DISMISS": "Dismiss",
"ERRORS_COURRUPTED_SDK_JAR_DEPENDENCY": "There was a problem with SuiteCloud Extension dependencies. Restart your Visual Studio Code instance.",
diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json
index 2416a6f9..226d12ba 100644
--- a/packages/vscode-extension/package.json
+++ b/packages/vscode-extension/package.json
@@ -111,7 +111,13 @@
"title": "Open DevAssist settings",
"category": "SuiteCloud",
"enablement": "false"
- }
+ },
+ {
+ "command": "suitecloud.opendevassistfeedbackform",
+ "title": "Open DevAssist Feedback Form",
+ "category": "SuiteCloud",
+ "enablement": "false"
+ }
],
"keybindings": [
{
diff --git a/packages/vscode-extension/resources/media/FeedbackForm.css b/packages/vscode-extension/resources/media/FeedbackForm.css
new file mode 100644
index 00000000..bd93cc24
--- /dev/null
+++ b/packages/vscode-extension/resources/media/FeedbackForm.css
@@ -0,0 +1,229 @@
+:root {
+ --gap: 12px;
+ --radius: 12px;
+ --border: #e5e7eb;
+ --text: #111827;
+ --muted: #6b7280
+}
+
+* {
+ box-sizing: border-box
+}
+
+body {
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial, sans-serif;
+ color: var(--text);
+ background: #f8fafc;
+ margin: 0;
+ padding: 24px
+}
+
+.card {
+ max-width: 760px;
+ margin: auto;
+ background: #fff;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ box-shadow: 0 10px 25px rgba(0, 0, 0, .05)
+}
+
+.card header {
+ padding: 20px 24px;
+ border-bottom: 1px solid var(--border)
+}
+
+h1 {
+ font-size: 20px;
+ margin: 0
+}
+
+.content {
+ padding: 20px 24px;
+ display: grid;
+ gap: var(--gap)
+}
+
+label {
+ font-weight: 600
+}
+
+textarea {
+ width: 100%;
+ min-height: 120px;
+ padding: 12px;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ resize: vertical;
+ font: inherit
+}
+
+fieldset {
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 12px
+}
+
+legend {
+ padding: 0 6px;
+ color: var(--muted)
+}
+
+.checks {
+ display: grid;
+ grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));
+ gap: 8px 14px;
+ margin-top: 6px
+}
+
+.checks label {
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 8px
+}
+
+.checks input {
+ transform: translateY(1px)
+}
+
+.stars {
+ display: inline-flex;
+ flex-direction: row-reverse; /* shows stars in correct LTR order */
+ gap: 6px;
+ width: fit-content;
+}
+.stars input {
+ display: none;
+}
+.star {
+ cursor: pointer;
+ font-size: 28px;
+ line-height: 1;
+ filter: grayscale(.6);
+ transition: transform .05s ease;
+}
+.star::before {
+ content: "☆";
+}
+/* On hover, fill all stars to the left (and including hovered) */
+.stars label.star:hover,
+.stars label.star:hover ~ label.star {
+ transform: scale(1.25);
+}
+.stars label.star:hover::before,
+.stars label.star:hover ~ label.star::before {
+ content: "★";
+ color: #f59e0b;
+ filter: none;
+}
+.stars label.star:hover,
+.stars label.star:hover ~ label.star {
+ color: #f59e0b;
+ filter: none;
+}
+
+/* On checked, fill all stars to the left (and including selected) */
+.stars input:checked ~ label.star::before,
+.stars input:checked ~ label.star {
+ content: "★";
+ color: #f59e0b;
+ filter: none;
+}
+
+.row {
+ display: grid;
+ gap: 6px
+}
+
+button {
+ appearance: none;
+ border: 1px solid var(--border);
+ background: #111827;
+ color: #fff;
+ padding: 10px 14px;
+ border-radius: 10px;
+ font-weight: 600;
+ cursor: pointer
+}
+
+button.secondary {
+ background: #fff;
+ color: #111827
+}
+
+.note {
+ color: var(--muted);
+ font-size: 12px
+}
+
+pre {
+ background: #0b1020;
+ color: #d1d5db;
+ padding: 12px;
+ border-radius: 10px;
+ overflow: auto;
+ max-height: 240px
+}
+
+.actions {
+ display: flex;
+ gap: 10px;
+ justify-content: flex-end;
+ margin-top: 8px
+}
+
+#alert-container {
+ position: fixed;
+ z-index: 9999;
+ bottom: 24px;
+ right: 24px;
+ display: flex;
+ flex-direction: column-reverse; /* new alerts on top */
+ gap: 12px;
+ max-width: 860px;
+ margin-left: 64px;
+ pointer-events: none;
+}
+.toast-alert {
+ display: flex;
+ align-items: start;
+ background: #e7f5ff;
+ color: #155577;
+ border-left: 4px solid #228be6;
+ border-radius: 6px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.13);
+ margin: 0;
+ padding: 16px 44px 16px 16px;
+ font-size: 15px;
+ position: relative;
+ min-width: 220px;
+ max-width: 100%;
+ pointer-events: auto;
+}
+.toast-alert.toast-info {
+ background: #e7f5ff;
+ border-color: #228be6;
+ color: #155577;
+}
+.toast-alert.toast-error {
+ background: #ffceca;
+ border-color: #ff4d4f;
+ color: #a8071a;
+}
+.toast-close {
+ position: absolute;
+ top: 8px;
+ right: 10px;
+ background: none;
+ border: none;
+ color: inherit;
+ font-size: 19px;
+ cursor: pointer;
+ line-height: 1;
+ padding: 0;
+ opacity: 0.65;
+ transition: opacity 0.19s;
+}
+.toast-close:hover {
+ opacity: 1;
+}
\ No newline at end of file
diff --git a/packages/vscode-extension/resources/media/FeedbackForm.html b/packages/vscode-extension/resources/media/FeedbackForm.html
new file mode 100644
index 00000000..3c59b534
--- /dev/null
+++ b/packages/vscode-extension/resources/media/FeedbackForm.html
@@ -0,0 +1,131 @@
+
+
+
+
+ DevAssist Feedback
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/vscode-extension/resources/media/FeedbackFormFailure.html b/packages/vscode-extension/resources/media/FeedbackFormFailure.html
new file mode 100644
index 00000000..7fc888cc
--- /dev/null
+++ b/packages/vscode-extension/resources/media/FeedbackFormFailure.html
@@ -0,0 +1,36 @@
+
+
+
+
+ DevAssist Feedback
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/vscode-extension/resources/media/FeedbackFormSubmitting.html b/packages/vscode-extension/resources/media/FeedbackFormSubmitting.html
new file mode 100644
index 00000000..ef087f44
--- /dev/null
+++ b/packages/vscode-extension/resources/media/FeedbackFormSubmitting.html
@@ -0,0 +1,34 @@
+
+
+
+
+ DevAssist Feedback
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/vscode-extension/resources/media/FeedbackFormSuccess.html b/packages/vscode-extension/resources/media/FeedbackFormSuccess.html
new file mode 100644
index 00000000..50d14677
--- /dev/null
+++ b/packages/vscode-extension/resources/media/FeedbackFormSuccess.html
@@ -0,0 +1,42 @@
+
+
+
+
+ DevAssist Feedback
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/vscode-extension/src/ApplicationConstants.ts b/packages/vscode-extension/src/ApplicationConstants.ts
index feab37f5..ec3b7e17 100644
--- a/packages/vscode-extension/src/ApplicationConstants.ts
+++ b/packages/vscode-extension/src/ApplicationConstants.ts
@@ -47,6 +47,7 @@ export const DEVASSIST = {
PROXY_URL: {
SCHEME: 'http://',
LOCALHOST_IP: '127.0.0.1',
- PATH: '/api/internal/devassist'
+ BASE_PATH: '/api/internal/devassist',
+ FEEDBACK_PATH: '/api/internal/devassist/feedback',
}
}
diff --git a/packages/vscode-extension/src/service/MediaFileKeys.ts b/packages/vscode-extension/src/service/MediaFileKeys.ts
new file mode 100644
index 00000000..1e369443
--- /dev/null
+++ b/packages/vscode-extension/src/service/MediaFileKeys.ts
@@ -0,0 +1,16 @@
+/*
+ ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+ */
+
+export const MEDIA_DIRECTORY = 'resources/media';
+
+export const FEEDBACK_FORM_FILE_NAMES = {
+ MAIN_PAGE : {
+ HTML : 'FeedbackForm.html',
+ CSS : 'FeedbackForm.css'
+ },
+ SUBMITTING_HTML : 'FeedbackFormSubmitting.html',
+ SUCCESS_HTML : 'FeedbackFormSuccess.html',
+ FAILURE_HTML : 'FeedbackFormFailure.html',
+}
\ No newline at end of file
diff --git a/packages/vscode-extension/src/service/MediaFileService.ts b/packages/vscode-extension/src/service/MediaFileService.ts
new file mode 100644
index 00000000..e6295ff8
--- /dev/null
+++ b/packages/vscode-extension/src/service/MediaFileService.ts
@@ -0,0 +1,33 @@
+/*
+ ** Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+ */
+
+import * as vscode from 'vscode';
+import * as path from 'path';
+import { FileUtils } from '../util/ExtensionUtil';
+import { MEDIA_DIRECTORY } from './MediaFileKeys';
+
+export class MediaFileService {
+ private vscodeExtensionMediaPath : string;
+
+ constructor(context: vscode.ExtensionContext) {
+ this.vscodeExtensionMediaPath = path.join(context.extensionPath, MEDIA_DIRECTORY);
+ }
+
+ public getMediaDirectoryFullPath = () => {
+ return this.vscodeExtensionMediaPath;
+ }
+
+ public getMediaFileFullPath = (fileName : string) => {
+ return path.join(this.vscodeExtensionMediaPath, fileName);
+ }
+
+ public generateHTMLContentFromMediaFile = (htmlFileName : string, cssWebviewUri : string) => {
+ const htmlFilePath = this.getMediaFileFullPath(htmlFileName);
+ let htmlFileContent = FileUtils.readAsString(htmlFilePath);
+
+ htmlFileContent = htmlFileContent.replace('{{CSS_FILE.css}}', cssWebviewUri);
+ return htmlFileContent;
+ }
+}
\ No newline at end of file
diff --git a/packages/vscode-extension/src/service/MessageService.ts b/packages/vscode-extension/src/service/MessageService.ts
index 4097051c..2fcbf66e 100644
--- a/packages/vscode-extension/src/service/MessageService.ts
+++ b/packages/vscode-extension/src/service/MessageService.ts
@@ -138,8 +138,12 @@ export default class MessageService {
private showOutputIfClicked = (message?: string) => {
if (message) {
- output.show();
+ this.showOutput();
}
}
+ public showOutput = () => {
+ output.show();
+ }
+
}
diff --git a/packages/vscode-extension/src/service/TranslationKeys.ts b/packages/vscode-extension/src/service/TranslationKeys.ts
index 87bbbf50..d7a2e4dd 100644
--- a/packages/vscode-extension/src/service/TranslationKeys.ts
+++ b/packages/vscode-extension/src/service/TranslationKeys.ts
@@ -20,9 +20,12 @@ export const ANSWERS = {
};
export const BUTTONS = {
+ DONT_SHOW_AGAIN: "BUTTONS_DONT_SHOW_AGAIN",
+ GIVE_FEEDBACK: "BUTTONS_GIVE_FEEDBACK",
+ OK: 'BUTTONS_OK',
+ OPEN_SETTINGS: "BUTTONS_OPEN_SETTINGS",
RESTART_NOW: 'BUTTONS_RESTART_NOW',
SEE_DETAILS: 'BUTTONS_SEE_DETAILS',
- OK: 'BUTTONS_OK'
};
export const COMMAND = {
@@ -105,6 +108,20 @@ export const DEVASSIST_SERVICE = {
SERVER_ERROR_ON_REFRESH: 'DEVASSIST_SERVICE_OUTPUT_SERVER_ERROR_ON_REFRESH',
}
},
+ FEEDBACK_FORM: {
+ GENERIC_VALIDATION_ERROR_WRAPPER: 'DEVASSIST_SERVICE_FEEDBACK_FORM_GENERIC_VALIDATION_ERROR_WRAPPER',
+ FIELD: {
+ VALIDATION_ERROR: 'DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_VALIDATION_ERROR',
+ CANNOT_BE_EMPTY_TEXT: 'DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_CANNOT_BE_EMPTY_TEXT',
+ CANNOT_BE_EMPTY_MULTIPLE_OPTION: 'DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_CANNOT_BE_EMPTY_MULTIPLE_OPTION',
+ CANNOT_BE_EMPTY_NUMERIC: 'DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_CANNOT_BE_EMPTY_NUMERIC',
+ CANNOT_BE_TOO_LONG: 'DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_CANNOT_BE_TOO_LONG',
+ CANNOT_HAVE_REPEATED_VALUES: 'DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_CANNOT_HAVE_REPEATED_VALUES',
+ MUST_HAVE_BE_A_VALID_NUMBER: 'DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_MUST_HAVE_BE_A_VALID_NUMBER',
+ MUST_HAVE_SPECIFIC_VALUES: 'DEVASSIST_SERVICE_FEEDBACK_FORM_FIELD_MUST_HAVE_SPECIFIC_VALUES',
+ },
+ SUBMITTING_ERROR: 'DEVASSIST_SERVICE_FEEDBACK_FORM_SUBMITTING_ERROR'
+ },
IS_DISABLED: {
NOTIFICATION: 'DEVASSIST_SERVICE_IS_DISABLED_NOTIFICATION',
OUTPUT: 'DEVASSIST_SERVICE_IS_DISABLED_OUTPUT',
@@ -120,14 +137,9 @@ export const DEVASSIST_SERVICE = {
STATUSBAR: 'DEVASSIST_SERVICE_IS_STOPPED_STATUSBAR'
},
STARTUP: {
- BUTTON: {
- DONT_SHOW_AGAIN: 'DEVASSIST_SERVICE_STARTUP_BUTTON_DONT_SHOW_AGAIN',
- OPEN_SETTINGS: 'DEVASSIST_SERVICE_STARTUP_BUTTON_OPEN_SETTINGS',
- },
MESSAGE: 'DEVASSIST_SERVICE_STARTUP_MESSAGE',
},
STATUSBAR_TOOLTIP: 'DEVASSIST_SERVICE_STATUSBAR_TOOLTIP',
- SEE_DETAILS_BUTTON: 'DEVASSIST_SERVICE_SEE_DETAILS_BUTTON'
}
export const DISMISS = 'DISMISS';
diff --git a/packages/vscode-extension/src/startup/DevAssistConfiguration.ts b/packages/vscode-extension/src/startup/DevAssistConfiguration.ts
index 86d2e15b..029b3891 100644
--- a/packages/vscode-extension/src/startup/DevAssistConfiguration.ts
+++ b/packages/vscode-extension/src/startup/DevAssistConfiguration.ts
@@ -3,7 +3,7 @@ import { DEVASSIST, VSCODE_PLATFORM } from '../ApplicationConstants';
import { getSdkPath } from '../core/sdksetup/SdkProperties';
import VSConsoleLogger from "../loggers/VSConsoleLogger";
import MessageService from '../service/MessageService';
-import { DEVASSIST_SERVICE, REFRESH_AUTHORIZATION } from '../service/TranslationKeys';
+import { BUTTONS, DEVASSIST_SERVICE, REFRESH_AUTHORIZATION } from '../service/TranslationKeys';
import { VSTranslationService } from '../service/VSTranslationService';
import { AuthenticationUtils, ExecutionEnvironmentContext, SuiteCloudAuthProxyService } from '../util/ExtensionUtil';
import { output } from '../suitecloud';
@@ -148,15 +148,16 @@ const initializeDevAssistService = (devAssistStatusBar: vscode.StatusBarItem) =>
const startDevAssistService = async (devAssistAuthID: string, localPort: number, devAssistStatusBar: vscode.StatusBarItem) => {
await devAssistProxyService.start(devAssistAuthID, localPort);
+ const proxyUrl = getProxyUrl(localPort);
setSuccessDevAssistStausBarMessage(devAssistStatusBar);
- const proxyUrl = getProxyUrl(localPort);
- vsNotificationService.showCommandInfo(translationService.getMessage(DEVASSIST_SERVICE.IS_RUNNING.NOTIFICATION, proxyUrl));
+ showDevAssistIsRunningNotification(proxyUrl);
+
vsLogger.printTimestamp();
vsLogger.info(translationService.getMessage(DEVASSIST_SERVICE.IS_RUNNING.OUTPUT, getProxyUrlWithoutPath(localPort), devAssistAuthID, proxyUrl));
}
const PROXY_URL = DEVASSIST.PROXY_URL;
-const getProxyUrl = (port: number) => `${PROXY_URL.SCHEME}${PROXY_URL.LOCALHOST_IP}:${port}${PROXY_URL.PATH}`;
+const getProxyUrl = (port: number) => `${PROXY_URL.SCHEME}${PROXY_URL.LOCALHOST_IP}:${port}${PROXY_URL.BASE_PATH}`;
const getProxyUrlWithoutPath = (port: number) => `${PROXY_URL.SCHEME}${PROXY_URL.LOCALHOST_IP}:${port}`;
const stopDevAssistService = (devAssistStatusBar: vscode.StatusBarItem) => {
@@ -178,11 +179,11 @@ const showDevAssistStartUpNotification = () => {
const infoMessage: string = translationService.getMessage(DEVASSIST_SERVICE.STARTUP.MESSAGE);
const buttonsAndActions: { buttonMessage: string, buttonAction: () => void }[] = [
{
- buttonMessage: translationService.getMessage(DEVASSIST_SERVICE.STARTUP.BUTTON.OPEN_SETTINGS),
+ buttonMessage: translationService.getMessage(BUTTONS.OPEN_SETTINGS),
buttonAction: openDevAssistSettings
},
{
- buttonMessage: translationService.getMessage(DEVASSIST_SERVICE.STARTUP.BUTTON.DONT_SHOW_AGAIN),
+ buttonMessage: translationService.getMessage(BUTTONS.DONT_SHOW_AGAIN),
buttonAction: () => {
const devAssistConfigSection = vscode.workspace.getConfiguration(DEVASSIST.CONFIG_KEYS.devAssistSection);
devAssistConfigSection.update(DEVASSIST.CONFIG_KEYS.startupNotificationDisabled, true);
@@ -192,6 +193,23 @@ const showDevAssistStartUpNotification = () => {
vsNotificationService.showCommandInfoWithSpecificButtonsAndActions(infoMessage, buttonsAndActions);
}
+const showDevAssistIsRunningNotification = (proxyUrl : string) => {
+ const infoMessage: string = translationService.getMessage(DEVASSIST_SERVICE.IS_RUNNING.NOTIFICATION, proxyUrl);
+ const buttonsAndActions: { buttonMessage: string, buttonAction: () => void }[] = [
+ {
+ buttonMessage: translationService.getMessage(BUTTONS.SEE_DETAILS),
+ buttonAction: vsNotificationService.showOutput
+ },
+ {
+ buttonMessage: translationService.getMessage(BUTTONS.GIVE_FEEDBACK),
+ buttonAction: () => {
+ vscode.commands.executeCommand('suitecloud.opendevassistfeedbackform')
+ }
+ },
+ ];
+ vsNotificationService.showCommandInfoWithSpecificButtonsAndActions(infoMessage, buttonsAndActions);
+}
+
const showStartDevAssistProblemNotification = (errorStage: string, error: string, devAssistStatusBar: vscode.StatusBarItem) => {
// console.log(`There was a problem when starting DevAssist service. (${errorStage})\n${error}`)
setErrorDevAssistStausBarMessage(devAssistStatusBar);
@@ -200,7 +218,7 @@ const showStartDevAssistProblemNotification = (errorStage: string, error: string
const errorMessage = translationService.getMessage(DEVASSIST_SERVICE.IS_STOPPED.NOTIFICATION);
const buttonsAndActions: { buttonMessage: string, buttonAction: () => void }[] = [
{
- buttonMessage: translationService.getMessage(DEVASSIST_SERVICE.SEE_DETAILS_BUTTON),
+ buttonMessage: translationService.getMessage(BUTTONS.SEE_DETAILS),
buttonAction: () => {
// show suitecloud output and devassist settings
output.show();
@@ -217,7 +235,7 @@ const showDevAssistEmitProblemLog = (errorStage: string, emitError: string, devA
const errorMessage = translationService.getMessage(DEVASSIST_SERVICE.IS_STOPPED.NOTIFICATION);
const buttonsAndActions: { buttonMessage: string, buttonAction: () => void }[] = [
{
- buttonMessage: translationService.getMessage(DEVASSIST_SERVICE.SEE_DETAILS_BUTTON),
+ buttonMessage: translationService.getMessage(BUTTONS.SEE_DETAILS),
buttonAction: () => output.show(),
},
];
@@ -232,7 +250,7 @@ const showDevAssistEmitProblemNotification = (errorStage: string, emitError: str
const errorMessage = translationService.getMessage(DEVASSIST_SERVICE.IS_STOPPED.NOTIFICATION);
const buttonsAndActions: { buttonMessage: string, buttonAction: () => void }[] = [
{
- buttonMessage: translationService.getMessage(DEVASSIST_SERVICE.SEE_DETAILS_BUTTON),
+ buttonMessage: translationService.getMessage(BUTTONS.SEE_DETAILS),
buttonAction: () => {
// show suitecloud output and devassist settings
output.show()
@@ -259,7 +277,7 @@ const updateDevAssistConfigStatus = (): void => {
devAssistConfigStatus.previous = previousConfig;
}
-const getDevAssistCurrentSettings = (): devAssistConfig => {
+export const getDevAssistCurrentSettings = (): devAssistConfig => {
const devAssistConfigSection = vscode.workspace.getConfiguration(DEVASSIST.CONFIG_KEYS.devAssistSection);
// * The *effective* value (returned by {@linkcode WorkspaceConfiguration.get get}) is computed by overriding or merging the values in the following order:
diff --git a/packages/vscode-extension/src/suitecloud.ts b/packages/vscode-extension/src/suitecloud.ts
index 38403896..d096df88 100644
--- a/packages/vscode-extension/src/suitecloud.ts
+++ b/packages/vscode-extension/src/suitecloud.ts
@@ -29,6 +29,7 @@ import { VSTranslationService } from './service/VSTranslationService';
import { devAssistConfigurationChangeHandler, startDevAssistProxyIfEnabled } from './startup/DevAssistConfiguration';
import { showSetupAccountWarningMessageIfNeeded } from './startup/ShowSetupAccountWarning';
import { createAuthIDStatusBar, createDevAssistStatusBar, createSuiteCloudProjectStatusBar, updateAuthIDStatusBarIfNeeded, updateStatusBars } from './startup/StatusBarItemsFunctions';
+import { openDevAssistFeedbackForm } from './webviews/FeedbackFormWebview';
const SCLOUD_OUTPUT_CHANNEL_NAME = 'SuiteCloud';
@@ -87,6 +88,13 @@ export async function activate(context: vscode.ExtensionContext) {
() => vscode.commands.executeCommand('workbench.action.openWorkspaceSettings', DEVASSIST.CONFIG_KEYS.devAssistSection))
);
+ // DevAssist Feedback Form WebView
+ context.subscriptions.push(
+ vscode.commands.registerCommand('suitecloud.opendevassistfeedbackform',
+ () => openDevAssistFeedbackForm(context)
+ )
+ );
+
// add watchers needed to update the status bars
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor((textEditor) => updateStatusBars(textEditor, suitecloudProjectStatusBar, authIDStatusBar)),
diff --git a/packages/vscode-extension/src/util/ExtensionUtil.ts b/packages/vscode-extension/src/util/ExtensionUtil.ts
index b026ee14..6eb81def 100644
--- a/packages/vscode-extension/src/util/ExtensionUtil.ts
+++ b/packages/vscode-extension/src/util/ExtensionUtil.ts
@@ -65,7 +65,12 @@ export class SuiteCloudAuthProxyService extends SuiteCloudAuthProxyServiceTypedJ
export const AccountCredentialsFormatter: {
getInfoString(accountCredentials: any): string;
} = require('@oracle/suitecloud-cli/src/utils/AccountCredentialsFormatter');
-export const FileUtils = require('@oracle/suitecloud-cli/src/utils/FileUtils');
+export const FileUtils : {
+ readAsJson(filePath: string) : any;
+ readAsString(fileName: string) : string;
+ exists(fileName: string) : boolean;
+ createDirectory(dirPath: string) : void;
+} = require('@oracle/suitecloud-cli/src/utils/FileUtils');
export const InteractiveAnswersValidator: {
showValidationResults(value: string, ...funcs: Function[]): string | boolean;
diff --git a/packages/vscode-extension/src/webviews/FeedbackFormWebview.ts b/packages/vscode-extension/src/webviews/FeedbackFormWebview.ts
new file mode 100644
index 00000000..f40322dd
--- /dev/null
+++ b/packages/vscode-extension/src/webviews/FeedbackFormWebview.ts
@@ -0,0 +1,202 @@
+/*
+ ** Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+ */
+
+import * as vscode from 'vscode';
+import { DEVASSIST } from '../ApplicationConstants';
+import { DEVASSIST_SERVICE } from '../service/TranslationKeys';
+import { MediaFileService } from '../service/MediaFileService';
+import { FEEDBACK_FORM_FILE_NAMES } from '../service/MediaFileKeys';
+import { VSTranslationService } from '../service/VSTranslationService';
+import { getDevAssistCurrentSettings } from '../startup/DevAssistConfiguration';
+import { ApplicationConstants } from '../util/ExtensionUtil';
+import VSConsoleLogger from '../loggers/VSConsoleLogger';
+import {
+ validateIntegerWithinInterval,
+ validateMultipleOptionField,
+ validateTextAreaField,
+} from './WebviewFieldValidationUtils';
+
+const translationService = new VSTranslationService();
+const vsLogger = new VSConsoleLogger();
+
+const PROXY_URL = DEVASSIST.PROXY_URL;
+
+const FEEDBACK_FORM_EVENTS = {
+ WEBVIEW_CONTROLLER: {
+ CLOSE : "CLOSE_WEBVIEW",
+ SUBMIT_FEEDBACK : "SUBMIT_FEEDBACK",
+ OPEN_NEW_FEEDBACK_FORM : "OPEN_NEW_FEEDBACK_FORM",
+ },
+ HTML_PAGE: {
+ RENDER_TOAST_MESSAGE : "RENDER_TOAST_MESSAGE",
+ }
+}
+
+type WebviewEventMessage = {
+ eventType: string;
+ eventData?: FeedbackFormData
+};
+
+type FeedbackFormData = {
+ feedback: string;
+ topics: string[];
+ rating: number;
+};
+
+const VALID_FEEDBACK_TOPICS = [
+ "CodeExplanation",
+ "SDFObjectGeneration",
+ "SuiteScriptCodeGeneration",
+ "UnitTesting",
+ "Other"
+]
+
+let feedbackFormPanel: vscode.WebviewPanel | undefined;
+let mediaService: MediaFileService;
+export const openDevAssistFeedbackForm = (context: vscode.ExtensionContext) => {
+
+ // if one FeedbackForm is already open, reveal it instead of creating a new one
+ if (feedbackFormPanel) {
+ feedbackFormPanel.reveal();
+ return;
+ }
+
+ mediaService = new MediaFileService(context);
+ feedbackFormPanel = vscode.window.createWebviewPanel(
+ 'devassistfeedbackform',
+ 'SuiteCloud Developer Assistant Feedback',
+ vscode.ViewColumn.One,
+ {
+ enableScripts: true,
+ localResourceRoots: [
+ vscode.Uri.file(mediaService.getMediaDirectoryFullPath()),
+ ],
+ },
+ );
+
+ // calculate cssUri as a proper Webview File Path and generate HTML content with it
+ const cssFilePath = mediaService.getMediaFileFullPath(FEEDBACK_FORM_FILE_NAMES.MAIN_PAGE.CSS);
+ const cssWebviewUri = feedbackFormPanel?.webview.asWebviewUri(vscode.Uri.file(cssFilePath)).toString();
+ feedbackFormPanel.webview.html = mediaService.generateHTMLContentFromMediaFile(FEEDBACK_FORM_FILE_NAMES.MAIN_PAGE.HTML, cssWebviewUri);
+
+ // Clean up the reference when the WebviewPanel is closed
+ feedbackFormPanel.onDidDispose(
+ () => {
+ feedbackFormPanel = undefined;
+ },
+ null,
+ context.subscriptions,
+ );
+
+ // Handle messages/events sent from HTML to this Webview controller
+ feedbackFormPanel.webview.onDidReceiveMessage(
+ (webviewEvent) => handleWebviewEventMessage(webviewEvent, cssWebviewUri),
+ undefined,
+ context.subscriptions,
+ );
+};
+
+const handleWebviewEventMessage = async (webviewEvent : WebviewEventMessage, cssWebviewUri : string) : Promise => {
+ switch (webviewEvent.eventType) {
+ case FEEDBACK_FORM_EVENTS.WEBVIEW_CONTROLLER.SUBMIT_FEEDBACK:
+ await handleSubmitFeedbackFormEvent(webviewEvent.eventData!, cssWebviewUri);
+ break;
+
+ case FEEDBACK_FORM_EVENTS.WEBVIEW_CONTROLLER.OPEN_NEW_FEEDBACK_FORM:
+ feedbackFormPanel!.webview.html = mediaService.generateHTMLContentFromMediaFile(FEEDBACK_FORM_FILE_NAMES.MAIN_PAGE.HTML, cssWebviewUri);
+ break;
+
+ case FEEDBACK_FORM_EVENTS.WEBVIEW_CONTROLLER.CLOSE:
+ feedbackFormPanel?.dispose();
+ break;
+ }
+}
+
+const handleSubmitFeedbackFormEvent = async (formData : FeedbackFormData, cssWebviewUri : string) => {
+
+ // validate Feedback Form Data
+ const validationResult = validateFormData(formData);
+ if (typeof validationResult === 'string') {
+ await sendErrorEventToWebview(translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.GENERIC_VALIDATION_ERROR_WRAPPER, validationResult));
+ return;
+ }
+
+ feedbackFormPanel!.webview.html = mediaService.generateHTMLContentFromMediaFile(FEEDBACK_FORM_FILE_NAMES.SUBMITTING_HTML, cssWebviewUri);
+
+ // Send request to NetSuite Backend through Proxy
+ try {
+ const currentProxySettings = getDevAssistCurrentSettings();
+ const requestBody = JSON.stringify(formData);
+ const response = await fetch(`${PROXY_URL.SCHEME}${PROXY_URL.LOCALHOST_IP}:${currentProxySettings.localPort}${PROXY_URL.FEEDBACK_PATH}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: requestBody
+ });
+
+ if (response.ok) {
+ vsLogger.printTimestamp();
+ vsLogger.info("Feedback Form Success: " + response.status + ' ' + response.statusText);
+ vsLogger.info('');
+ feedbackFormPanel!.webview.html = mediaService.generateHTMLContentFromMediaFile(FEEDBACK_FORM_FILE_NAMES.SUCCESS_HTML, cssWebviewUri);
+ }
+ else {
+ vsLogger.printTimestamp();
+ vsLogger.error("Feedback Form External Failure: " + response.status + ' ' + response.statusText);
+ vsLogger.error('');
+
+ // "Manual reauthentication is needed" proxy event
+ if (response.status === ApplicationConstants.HTTP_RESPONSE_CODE.FORBIDDEN) {
+ const responseBody : any = await response.json();
+ feedbackFormPanel!.webview.html = mediaService.generateHTMLContentFromMediaFile(FEEDBACK_FORM_FILE_NAMES.MAIN_PAGE.HTML, cssWebviewUri);
+ await sendErrorEventToWebview(`Error 403: "${responseBody.error}"`);
+ }
+ else {
+ feedbackFormPanel!.webview.html = mediaService.generateHTMLContentFromMediaFile(FEEDBACK_FORM_FILE_NAMES.FAILURE_HTML, cssWebviewUri);
+ }
+ }
+ } catch (e) {
+ vsLogger.printTimestamp();
+ vsLogger.error("Feedback Form Internal Failure: " + e);
+ vsLogger.error('');
+
+ // TODO: Find a way to not delete the user input when swaping HTML / clicking out
+ // -> https://code.visualstudio.com/api/extension-guides/webview#getstate-and-setstate
+ feedbackFormPanel!.webview.html = mediaService.generateHTMLContentFromMediaFile(FEEDBACK_FORM_FILE_NAMES.MAIN_PAGE.HTML, cssWebviewUri);
+ await sendErrorEventToWebview(translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.SUBMITTING_ERROR));
+ }
+}
+
+
+const sendErrorEventToWebview = async (message: string) => {
+ await feedbackFormPanel!.webview.postMessage({
+ eventType: FEEDBACK_FORM_EVENTS.HTML_PAGE.RENDER_TOAST_MESSAGE,
+ eventData: {
+ toastMessageLevel : 'error',
+ toastMessageContent: message}
+ });
+}
+
+const validateFormData = (formData : FeedbackFormData) => {
+
+ // validate feedback field (textArea)
+ let validationResult = validateTextAreaField("Your feedback (text area)", formData.feedback, 1000);
+ if (typeof validationResult === 'string') {
+ return validationResult;
+ }
+
+ // validate selectedTopic field
+ validationResult = validateMultipleOptionField("Your feedback (topic multi-choice selector)", formData.topics, VALID_FEEDBACK_TOPICS);
+ if (typeof validationResult === 'string') {
+ return validationResult;
+ }
+
+ // validate rating field (integer 0 < x <= 5)
+ validationResult = validateIntegerWithinInterval("Rating", formData.rating, 1, 5);
+ if (typeof validationResult === 'string') {
+ return validationResult;
+ }
+
+ return true;
+}
\ No newline at end of file
diff --git a/packages/vscode-extension/src/webviews/WebviewFieldValidationUtils.ts b/packages/vscode-extension/src/webviews/WebviewFieldValidationUtils.ts
new file mode 100644
index 00000000..0e8909b5
--- /dev/null
+++ b/packages/vscode-extension/src/webviews/WebviewFieldValidationUtils.ts
@@ -0,0 +1,65 @@
+/*
+ ** Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
+ */
+
+import { DEVASSIST_SERVICE } from '../service/TranslationKeys';
+import { VSTranslationService } from '../service/VSTranslationService';
+
+const translationService = new VSTranslationService();
+
+export const validateTextAreaField = (fieldName : string, textContent : string, maxLength : number) => {
+ if (!textContent || textContent.trim().length === 0)
+ {
+ return translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.VALIDATION_ERROR,
+ fieldName,
+ translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.CANNOT_BE_EMPTY_TEXT));
+ }
+ else if (textContent.length > maxLength) {
+ return translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.VALIDATION_ERROR,
+ fieldName,
+ translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.CANNOT_BE_TOO_LONG, maxLength.toString()));
+ }
+
+ return true;
+}
+
+export const validateMultipleOptionField = (fieldName : string, selectedOptions : string[], acceptableOptions : string[]) => {
+ if (!selectedOptions || selectedOptions.length === 0) {
+ return translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.VALIDATION_ERROR,
+ fieldName,
+ translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.CANNOT_BE_EMPTY_MULTIPLE_OPTION));
+ }
+ else {
+ const uniqueSelectedOptions = new Set(selectedOptions);
+ if (uniqueSelectedOptions.size !== selectedOptions.length) {
+ return translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.VALIDATION_ERROR,
+ fieldName,
+ translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.CANNOT_HAVE_REPEATED_VALUES, selectedOptions.toString()));
+ }
+ for (const option of selectedOptions) {
+ if (acceptableOptions.includes(option)) {
+ return translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.VALIDATION_ERROR,
+ fieldName,
+ translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.MUST_HAVE_SPECIFIC_VALUES, acceptableOptions.toString()));
+ }
+ }
+ }
+
+ return true;
+}
+
+export const validateIntegerWithinInterval = (fieldName : string, nValue : number, lowerBound : number, upperBound : number) => {
+ if (!nValue || nValue === 0) {
+ return translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.VALIDATION_ERROR,
+ fieldName,
+ translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.CANNOT_BE_EMPTY_NUMERIC));
+ }
+ else if (!Number.isInteger(nValue) || nValue < lowerBound || nValue > upperBound) {
+ return translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.VALIDATION_ERROR,
+ fieldName,
+ translationService.getMessage(DEVASSIST_SERVICE.FEEDBACK_FORM.FIELD.MUST_HAVE_SPECIFIC_VALUES, lowerBound.toString(), upperBound.toString()));
+ }
+
+ return true;
+}
\ No newline at end of file