diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e6a615a..87cb1cc9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,9 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- New UI for the basic mode Sync (#415)
- Allow changing namespaces and IPM package context from web UI (#280)
+- Support for editing repo from filesystem perspective via web application (#464)
+- Support for downloading a VSCode workspace file from web UI
### Fixed
- Instance wide settings are placed in proper global (#444)
+- Avoid delay/errors in loading interop JS when there is a URL prefix (e.g., instance name in multi-instance webserver configuration)
+- Added proper JS escaping in sync output
- Added support to switch branch in basic mode from menu (#451)
## [2.4.1] - 2024-08-02
diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls
index dacf2816..f4eab0b3 100644
--- a/cls/SourceControl/Git/Extension.cls
+++ b/cls/SourceControl/Git/Extension.cls
@@ -74,9 +74,13 @@ Method AfterUserAction(Type As %Integer, Name As %String, InternalName As %Strin
if menu '= "%SourceMenu", menu'="%SourceContext" {
quit $$$OK
}
- set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName)
+ set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName, .fromWebApp)
set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName)
set ec = ##class(SourceControl.Git.Utils).AfterUserAction(Type, Name, InternalName, .Answer, .Msg, .Reload)
+ if fromWebApp {
+ // Force reload and compile of actual item if underlying file has changed
+ do ..OnBeforeLoad(InternalName,1,1)
+ }
quit ec
}
@@ -235,12 +239,12 @@ Method OnMenuItem(MenuName As %String, InternalName As %String, SelectedText As
/// This is called before the actual load of data to give the chance
/// to load the item from an external format.
-Method OnBeforeLoad(InternalName As %String, verbose As %Boolean) As %Status
+Method OnBeforeLoad(InternalName As %String, verbose As %Boolean, compile As %Boolean = 0) As %Status
{
set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName)
set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName)
if ##class(SourceControl.Git.Utils).IsInSourceControl(InternalName) {
- quit ##class(SourceControl.Git.Utils).ImportItem(InternalName,,0)
+ quit ##class(SourceControl.Git.Utils).ImportItem(InternalName,,0,compile)
}
quit $$$OK
}
@@ -274,14 +278,21 @@ Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NU
{
set sc = $$$OK
try {
- set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(.InternalName)
+ set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(.InternalName,.fromWebApp,.fullExternalName)
set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName)
if ##class(SourceControl.Git.Utils).IsNamespaceInGit() && ..IsInSourceControl(InternalName) {
- set filename = ##class(SourceControl.Git.Utils).FullExternalName(InternalName)
- $$$ThrowOnError(##class(SourceControl.Git.Utils).RemoveRoutineTSH(InternalName))
- $$$ThrowOnError(##class(SourceControl.Git.Utils).ExportItem(InternalName))
- if '##class(SourceControl.Git.Change).IsUncommitted(filename) {
- $$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "edit", InternalName, $username, "", 1, "", "", 0))
+ if fromWebApp {
+ if fullExternalName = ##class(SourceControl.Git.Utils).FullExternalName(InternalName) {
+ // Reimport item into database
+ $$$ThrowOnError(##class(SourceControl.Git.Utils).ImportItem(InternalName,,1,1))
+ }
+ } else {
+ set filename = ##class(SourceControl.Git.Utils).FullExternalName(InternalName)
+ $$$ThrowOnError(##class(SourceControl.Git.Utils).RemoveRoutineTSH(InternalName))
+ $$$ThrowOnError(##class(SourceControl.Git.Utils).ExportItem(InternalName))
+ if '##class(SourceControl.Git.Change).IsUncommitted(filename) {
+ $$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "edit", InternalName, $username, "", 1, "", "", 0))
+ }
}
}
} catch e {
diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls
index 6a4ce435..da9fdddf 100644
--- a/cls/SourceControl/Git/Settings.cls
+++ b/cls/SourceControl/Git/Settings.cls
@@ -47,6 +47,9 @@ Property defaultMergeBranch As %String [ InitialExpression = {##class(SourceCont
/// Compile using the configured pull event handler when "Import All" is run
Property compileOnImport As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).CompileOnImport()} ];
+/// Define a namespace-level web application allowing access to multiple git repos across separate namespaces
+Property namespaceLevelGitWebApp As %Boolean [ InitialExpression = {##class(SourceControl.Git.Settings).HasNamespaceWebApp()} ];
+
Property Mappings [ MultiDimensional ];
Method %OnNew() As %Status
@@ -160,6 +163,75 @@ ClassMethod Configure() As %Boolean [ CodeMode = objectgenerator ]
do %code.WriteLine(" quit 1")
}
+Method ConfigureNamespaceWebApplication()
+{
+ Set root = ##class(%Library.File).NormalizeDirectory(##class(SourceControl.Git.Utils).TempFolder())
+ Set deleteWebApp = ..HasNamespaceWebApp(.appDirectory) && '..namespaceLevelGitWebApp
+ Set createWebApp = ..namespaceLevelGitWebApp && '..HasNamespaceWebApp()
+ Do ..WebAppOperation("/git/"_$Namespace_"/", createWebApp, deleteWebApp, root)
+}
+
+Method WebAppOperation(name, create As %Boolean, delete As %Boolean, root As %String) [ Private ]
+{
+ Set namespace = $Namespace
+ New $Namespace
+ Set $Namespace = "%SYS"
+ If $Extract(name) = "/" {
+ Set name = $Extract(name,1,*-1)
+ }
+ If delete {
+ If ##class(Security.Applications).Exists(name) {
+ $$$ThrowOnError(##class(Security.Applications).Delete(name))
+ Write !,"Removed web application "_name
+ }
+ Quit
+ }
+
+ // These are the only things we want to coerce.
+ Set props("AutheEnabled")=0 // No auth methods enabled = impossible to use
+ Set props("InbndWebServicesEnabled")=0
+ Set props("ServeFiles")=0
+ Set props("Enabled")=1
+ Set props("Name")=name
+ Set props("NameSpace")=namespace
+ Set props("Path")=root
+ Set props("Type")=2
+ Set props("Recurse")=1
+ If create {
+ Write !,"Creating web application: "_name_"... "
+ $$$ThrowOnError(##class(Security.Applications).Create(name,.props))
+ Write "done."
+ } ElseIf ##class(Security.Applications).Exists(name) {
+ Write !,"Web application '"_name_"' already exists."
+ $$$ThrowOnError(##class(Security.Applications).Get(name,.existingProps))
+ Set changes = 0
+ Set key = ""
+ For {
+ Set key = $Order(props(key),1,value)
+ Quit:key=""
+ If (value '= $Get(existingProps(key))) {
+ Write !,"Changing "_key_": "_$Get(existingProps(key))_" -> "_value
+ Set changes = 1
+ }
+ }
+ If changes {
+ $$$ThrowOnError(##class(Security.Applications).Modify(name,.props))
+ Write !,"Web application '"_name_"' updated."
+ } Else {
+ Write !,"No changes made to web application."
+ }
+ }
+}
+
+ClassMethod HasNamespaceWebApp(Output webAppDirectory) As %Boolean
+{
+ Set webAppDirectory = $System.CSP.GetFileName("/git/"_$Namespace_"/")
+ If (webAppDirectory '= "") {
+ Set webAppDirectory = ##class(%Library.File).NormalizeDirectory(webAppDirectory)
+ }
+ Quit (webAppDirectory '= "")
+}
+
Method OnAfterConfigure() As %Boolean
{
set defaultPromptFlag = $$$DisableBackupCharMask + $$$TrapCtrlCMask + $$$EnableQuitCharMask + $$$DisableHelpCharMask + $$$DisableHelpContextCharMask + $$$TrapErrorMask
@@ -186,6 +258,8 @@ Method OnAfterConfigure() As %Boolean
}
}
+ do ..ConfigureNamespaceWebApplication()
+
set gitDir = ##class(%File).NormalizeDirectory(..namespaceTemp)_".git"
if '##class(%File).DirectoryExists(gitDir) {
set list(1) = "Initialize empty repo"
diff --git a/cls/SourceControl/Git/StreamServer.cls b/cls/SourceControl/Git/StreamServer.cls
index daf75a8d..42b2330e 100644
--- a/cls/SourceControl/Git/StreamServer.cls
+++ b/cls/SourceControl/Git/StreamServer.cls
@@ -7,7 +7,7 @@ Class SourceControl.Git.StreamServer Extends %CSP.StreamServer
ClassMethod OnPage() As %Status
{
if (%stream '= $$$NULLOREF) && $data(%base)#2 {
- set sourceControlInclude = ##class(SourceControl.Git.Utils).GetSourceControlInclude()
+ set sourceControlInclude = ##class(SourceControl.Git.Utils).GetSourceControlInclude(%request.URLPrefix)
while '%stream.AtEnd {
set text = %stream.Read()
set text = $replace(text,"{{baseHref}}",..EscapeHTML(%base))
@@ -19,4 +19,3 @@ ClassMethod OnPage() As %Status
}
}
-
diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls
index 9b6405c7..94290b60 100644
--- a/cls/SourceControl/Git/Utils.cls
+++ b/cls/SourceControl/Git/Utils.cls
@@ -1116,7 +1116,7 @@ ClassMethod FullExternalName(ByRef InternalName As %String, ByRef MappingExists
..TempFolder()_..ExternalName(.InternalName, .MappingExists)
}
-ClassMethod NormalizeInternalName(ByRef name As %String) As %String
+ClassMethod NormalizeInternalName(ByRef name As %String, Output fromWebApp As %Boolean = 0, Output cspFilename) As %String
{
//Studio passes name of routine with dots as it is in folders
//e.g. Package.SubPackage.Routine.mac has InternalName = /Package/SubPackage/Routine.mac
@@ -1132,6 +1132,16 @@ ClassMethod NormalizeInternalName(ByRef name As %String) As %String
if ($extract(name) '= "/") && (type'="csp") {
quit $piece(name,".",1,*-1)_"."_$zconvert($piece(name,".",*),"U")
}
+
+ if (name [ "/") && (type = "csp") {
+ set cspFilename = $System.CSP.GetFileName(name)
+ if (cspFilename '= "") && (cspFilename [ ..TempFolder()) {
+ set name = ..NameToInternalName(cspFilename)
+ set fromWebApp = 1
+ } else {
+ kill cspFilename
+ }
+ }
if (type = "inc") || (type = "mac") || (type = "int") {
set name = $extract($translate(name, "/", "."), 2, *)
@@ -1246,7 +1256,7 @@ ClassMethod FixProjectCspReferences(projectName As %String) As %Status
/// imports file if version in system is newer then version on disk.
/// if force = 1 then imports in any case.
-ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose As %Boolean = 1) As %Status
+ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose As %Boolean = 1, compile As %Boolean = 0) As %Status
{
#dim filename as %String = ..FullExternalName(.InternalName)
#dim fileTSH = ##class(%File).GetFileDateModified(filename)
@@ -1265,7 +1275,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A
if ($extract(InternalName, 1) = "/"){
set sc = ..ImportCSPFile(InternalName)
} else{
- set sc = $system.OBJ.Load(filename,"-l-d")
+ set sc = $system.OBJ.Load(filename,$Select(compile:"ck-l",1:"-l-d"))
}
}
if sc {
@@ -2396,10 +2406,10 @@ ClassMethod GetPackageVersion() As %String [ CodeMode = objectgenerator ]
quit $$$OK
}
-ClassMethod GetSourceControlInclude() As %String
+ClassMethod GetSourceControlInclude(prefix As %String = "") As %String
{
quit $select(##class(%Library.EnsembleMgr).IsEnsembleInstalled():
- "",
+ "",
1: "")
}
@@ -2615,4 +2625,3 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status
}
}
-
diff --git a/cls/SourceControl/Git/WebUIDriver.cls b/cls/SourceControl/Git/WebUIDriver.cls
index 5d8a2b17..d5580448 100644
--- a/cls/SourceControl/Git/WebUIDriver.cls
+++ b/cls/SourceControl/Git/WebUIDriver.cls
@@ -27,6 +27,65 @@ ClassMethod HandleRequest(pagePath As %String, InternalName As %String = "", Out
if $isobject($get(responseJSON)) {
do responseJSON.%ToJSON(%data)
}
+ } elseif pathStart = "vscode-workspace" {
+ set handled = 1
+ set namespace = $Namespace
+ set instanceName = $ZConvert(##class(%SYS.System).InstanceGUID(),"L")
+ set hasCSP = 0
+ if context.IsInGitEnabledPackage && 'context.IsInDefaultPackage {
+ set workspaceFilename = context.Package.Name_"-"_instanceName_".code-workspace"
+ // Loading an IPM package in dev mode automatically creates a project for it, so filter to that.
+ set filter = "?project="_context.Package.Name
+ } else {
+ set hasCSP = ##class(SourceControl.Git.Settings).HasNamespaceWebApp()
+ set workspaceFilename = namespace_"-"_instanceName_".code-workspace"
+ set filter = "?mapped=0"
+ }
+ set server = {
+ "webServer": {
+ "host": (%request.CgiEnvs("SERVER_NAME")),
+ "port": (+%request.CgiEnvs("SERVER_PORT")),
+ "scheme": ($Select(%request.Secure:"https",1:"http"))
+ },
+ "username": ($Username)
+ }
+ if (%request.URLPrefix '= "") {
+ set server.webServer.pathPrefix = %request.URLPrefix
+ }
+ set workspaceDef = {
+ "folders": [
+ ],
+ "settings": {
+ "intersystems.servers": {
+ "/default": (instanceName)
+ },
+ "objectscript.conn": {
+ "active": false
+ }
+ }
+ }
+ if hasCSP {
+ do workspaceDef.folders.%Push({
+ "name": ($Namespace),
+ "uri": ("isfs://"_instanceName_":"_$Namespace_"/git/"_$Namespace_"?csp")
+ })
+ } else {
+ do workspaceDef.folders.%Push({
+ "name": ($Namespace),
+ "uri": ("isfs://"_instanceName_":"_$Namespace_"/"_filter)
+ })
+ }
+ set $Property(workspaceDef.settings."intersystems.servers",instanceName) = server
+ set stream = ##class(%CSP.CharacterStream).%New()
+ do stream.SetAttribute("ContentDisposition","attachment; filename="_workspaceFilename)
+ set formatter = ##class(%JSON.Formatter).%New()
+ do formatter.FormatToStream(workspaceDef,stream)
+ $$$ThrowOnError(stream.%Save())
+ do %request.Set("STREAMOID",##class(%CSP.Page).Encrypt(stream.%Oid()))
+ set %request.Data("EXPIRES",1,"encrypt") = 1
+ do %request.Set("EXPIRES",0)
+ do ##class(%CSP.StreamServer).OnPreHTTP() // Need to call this to set headers properly
+ set %stream = 1 // Leak this to webuidriver.csp
} elseif $match(pathStart,"git-command|git|dirname|hostname|viewonly|contexts") {
if (%request.Method = "GET") {
set %data = ##class(%Stream.TmpCharacter).%New()
diff --git a/csp/sync.csp b/csp/sync.csp
index 78be9c2c..bab0d58d 100644
--- a/csp/sync.csp
+++ b/csp/sync.csp
@@ -146,7 +146,7 @@
&js<
var outputContainer = document.getElementById('outputBox');
- var lineText = "#(escapedLine)#";
+ var lineText = #(..QuoteJS(escapedLine))#;
var lineTextNode = document.createTextNode(lineText);
outputContainer.innerHTML += lineText + "
";
>
diff --git a/git-webui/release/share/git-webui/webui/css/git-webui.css b/git-webui/release/share/git-webui/webui/css/git-webui.css
index 0a0043b7..0c75c1f4 100644
--- a/git-webui/release/share/git-webui/webui/css/git-webui.css
+++ b/git-webui/release/share/git-webui/webui/css/git-webui.css
@@ -183,7 +183,7 @@ body {
padding-bottom: 100px;
}
#sidebar #sidebar-content > :first-child,
-#sidebar #sidebar-content > :nth-last-child(2) {
+#sidebar #sidebar-content > :nth-last-child(3) {
border-top: 1px solid #5e5e5e;
}
#sidebar #sidebar-content h4:before {
@@ -216,17 +216,36 @@ body {
#sidebar #sidebar-content #sidebar-tags h4:before {
content: url(../img/tag.svg);
}
+#sidebar #sidebar-content #sidebar-vscode a {
+ color: white;
+}
+#sidebar #sidebar-content #sidebar-vscode h4 {
+ padding: 0px;
+}
+#sidebar #sidebar-content #sidebar-vscode h4:before {
+ content: url(../img/file.svg);
+}
+#sidebar #sidebar-content #sidebar-vscode {
+ position: absolute;
+ bottom: 80px;
+ width: 16.7em;
+ background-color: #333333;
+}
+#sidebar #sidebar-content #sidebar-context h4 {
+ padding: 0px;
+}
#sidebar #sidebar-content #sidebar-context h4:before {
content: url(../img/context.svg);
}
#sidebar #sidebar-content #sidebar-context {
position: absolute;
- bottom: 50px;
+ bottom: 40px;
width: 16.7em;
- padding-bottom: 0.3rem;
- margin-bottom: 0;
background-color: #333333;
}
+#sidebar #sidebar-content #sidebar-settings h4 {
+ padding: 0px;
+}
#sidebar #sidebar-content #sidebar-settings h4:before {
content: url(../img/gear-fill.svg);
}
@@ -234,8 +253,6 @@ body {
position: absolute;
bottom: 0px;
width: 16.7em;
- padding-bottom: 0.3rem;
- margin-bottom: 0px;
background-color: #333333;
}
#sidebar #sidebar-content ul {
diff --git a/git-webui/release/share/git-webui/webui/img/context.svg b/git-webui/release/share/git-webui/webui/img/context.svg
index e3732172..f5ef6c98 100644
--- a/git-webui/release/share/git-webui/webui/img/context.svg
+++ b/git-webui/release/share/git-webui/webui/img/context.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/git-webui/release/share/git-webui/webui/js/git-webui.js b/git-webui/release/share/git-webui/webui/js/git-webui.js
index 9625c506..632c10f1 100644
--- a/git-webui/release/share/git-webui/webui/js/git-webui.js
+++ b/git-webui/release/share/git-webui/webui/js/git-webui.js
@@ -421,6 +421,9 @@ webui.SideBarView = function(mainView, noEventHandlers) {
if (id.indexOf("local-branches") > -1) {
// parses the output of git branch --verbose --verbose
var matches = /^\*?\s*([\w-\/]+)\s+([^\s]+)\s+(\[.*\])?.*/.exec(ref);
+ if (!matches) {
+ continue;
+ }
var branchInfo = {
"branch_name": matches[1],
"hash": matches[2],
@@ -973,6 +976,9 @@ webui.SideBarView = function(mainView, noEventHandlers) {
'' +
+ '' +
'' +
diff --git a/git-webui/src/share/git-webui/webui/css/git-webui.less b/git-webui/src/share/git-webui/webui/css/git-webui.less
index 652d5bdb..8fb86733 100644
--- a/git-webui/src/share/git-webui/webui/css/git-webui.less
+++ b/git-webui/src/share/git-webui/webui/css/git-webui.less
@@ -257,7 +257,7 @@ body {
color: @gray-lighter;
padding-bottom: 100px;
- > :first-child, >:nth-last-child(2) {
+ > :first-child, >:nth-last-child(3) {
border-top: 1px solid darken(@separator-line, 50%);
}
@@ -301,19 +301,44 @@ body {
content: url(../img/tag.svg);
}
+ #sidebar-vscode a {
+ color: white;
+ }
+
+ #sidebar-vscode h4 {
+ padding: 0px;
+ }
+
+ #sidebar-vscode h4:before {
+ content: url(../img/file.svg);
+ }
+
+ #sidebar-vscode {
+ position: absolute;
+ bottom: 80px;
+ width: 16.7em;
+ background-color: rgba(51, 51, 51, 1);
+ }
+
+ #sidebar-context h4 {
+ padding: 0px;
+ }
+
#sidebar-context h4:before {
- content: url(../img/context.svg)
+ content: url(../img/context.svg);
}
#sidebar-context {
position: absolute;
- bottom: 50px;
+ bottom: 40px;
width: 16.7em;
- padding-bottom: 0.3rem;
- margin-bottom: 0;
background-color: rgba(51, 51, 51, 1);
}
+ #sidebar-settings h4 {
+ padding: 0px;
+ }
+
#sidebar-settings h4:before {
content: url(../img/gear-fill.svg);
}
@@ -322,8 +347,6 @@ body {
position: absolute;
bottom: 0px;
width: 16.7em;
- padding-bottom: 0.3rem;
- margin-bottom:0px;
background-color: rgba(51, 51, 51, 1);
}
diff --git a/git-webui/src/share/git-webui/webui/img/context.svg b/git-webui/src/share/git-webui/webui/img/context.svg
index e3732172..f5ef6c98 100644
--- a/git-webui/src/share/git-webui/webui/img/context.svg
+++ b/git-webui/src/share/git-webui/webui/img/context.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/git-webui/src/share/git-webui/webui/js/git-webui.js b/git-webui/src/share/git-webui/webui/js/git-webui.js
index 9625c506..632c10f1 100644
--- a/git-webui/src/share/git-webui/webui/js/git-webui.js
+++ b/git-webui/src/share/git-webui/webui/js/git-webui.js
@@ -421,6 +421,9 @@ webui.SideBarView = function(mainView, noEventHandlers) {
if (id.indexOf("local-branches") > -1) {
// parses the output of git branch --verbose --verbose
var matches = /^\*?\s*([\w-\/]+)\s+([^\s]+)\s+(\[.*\])?.*/.exec(ref);
+ if (!matches) {
+ continue;
+ }
var branchInfo = {
"branch_name": matches[1],
"hash": matches[2],
@@ -973,6 +976,9 @@ webui.SideBarView = function(mainView, noEventHandlers) {
'' +
+ '' +
'' +