Skip to content

Commit 82d740a

Browse files
authored
Merge pull request #466 from intersystems/fix-464
Allow editing of files via web application
2 parents c6dd4d1 + 4919143 commit 82d740a

File tree

13 files changed

+241
-33
lines changed

13 files changed

+241
-33
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- New UI for the basic mode Sync (#415)
1212
- Allow changing namespaces and IPM package context from web UI (#280)
13+
- Support for editing repo from filesystem perspective via web application (#464)
14+
- Support for downloading a VSCode workspace file from web UI
1315

1416
### Fixed
1517
- Instance wide settings are placed in proper global (#444)
18+
- Avoid delay/errors in loading interop JS when there is a URL prefix (e.g., instance name in multi-instance webserver configuration)
19+
- Added proper JS escaping in sync output
1620
- Added support to switch branch in basic mode from menu (#451)
1721

1822
## [2.4.1] - 2024-08-02

cls/SourceControl/Git/Extension.cls

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,13 @@ Method AfterUserAction(Type As %Integer, Name As %String, InternalName As %Strin
7474
if menu '= "%SourceMenu", menu'="%SourceContext" {
7575
quit $$$OK
7676
}
77-
set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName)
77+
set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName, .fromWebApp)
7878
set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName)
7979
set ec = ##class(SourceControl.Git.Utils).AfterUserAction(Type, Name, InternalName, .Answer, .Msg, .Reload)
80+
if fromWebApp {
81+
// Force reload and compile of actual item if underlying file has changed
82+
do ..OnBeforeLoad(InternalName,1,1)
83+
}
8084
quit ec
8185
}
8286

@@ -235,12 +239,12 @@ Method OnMenuItem(MenuName As %String, InternalName As %String, SelectedText As
235239

236240
/// This is called before the actual load of data to give the chance
237241
/// to load the item from an external format.
238-
Method OnBeforeLoad(InternalName As %String, verbose As %Boolean) As %Status
242+
Method OnBeforeLoad(InternalName As %String, verbose As %Boolean, compile As %Boolean = 0) As %Status
239243
{
240244
set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName)
241245
set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName)
242246
if ##class(SourceControl.Git.Utils).IsInSourceControl(InternalName) {
243-
quit ##class(SourceControl.Git.Utils).ImportItem(InternalName,,0)
247+
quit ##class(SourceControl.Git.Utils).ImportItem(InternalName,,0,compile)
244248
}
245249
quit $$$OK
246250
}
@@ -274,14 +278,21 @@ Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NU
274278
{
275279
set sc = $$$OK
276280
try {
277-
set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(.InternalName)
281+
set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(.InternalName,.fromWebApp,.fullExternalName)
278282
set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName)
279283
if ##class(SourceControl.Git.Utils).IsNamespaceInGit() && ..IsInSourceControl(InternalName) {
280-
set filename = ##class(SourceControl.Git.Utils).FullExternalName(InternalName)
281-
$$$ThrowOnError(##class(SourceControl.Git.Utils).RemoveRoutineTSH(InternalName))
282-
$$$ThrowOnError(##class(SourceControl.Git.Utils).ExportItem(InternalName))
283-
if '##class(SourceControl.Git.Change).IsUncommitted(filename) {
284-
$$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "edit", InternalName, $username, "", 1, "", "", 0))
284+
if fromWebApp {
285+
if fullExternalName = ##class(SourceControl.Git.Utils).FullExternalName(InternalName) {
286+
// Reimport item into database
287+
$$$ThrowOnError(##class(SourceControl.Git.Utils).ImportItem(InternalName,,1,1))
288+
}
289+
} else {
290+
set filename = ##class(SourceControl.Git.Utils).FullExternalName(InternalName)
291+
$$$ThrowOnError(##class(SourceControl.Git.Utils).RemoveRoutineTSH(InternalName))
292+
$$$ThrowOnError(##class(SourceControl.Git.Utils).ExportItem(InternalName))
293+
if '##class(SourceControl.Git.Change).IsUncommitted(filename) {
294+
$$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "edit", InternalName, $username, "", 1, "", "", 0))
295+
}
285296
}
286297
}
287298
} catch e {

cls/SourceControl/Git/Settings.cls

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ Property defaultMergeBranch As %String [ InitialExpression = {##class(SourceCont
4747
/// Compile using the configured pull event handler when "Import All" is run
4848
Property compileOnImport As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).CompileOnImport()} ];
4949

50+
/// Define a namespace-level web application allowing access to multiple git repos across separate namespaces
51+
Property namespaceLevelGitWebApp As %Boolean [ InitialExpression = {##class(SourceControl.Git.Settings).HasNamespaceWebApp()} ];
52+
5053
Property Mappings [ MultiDimensional ];
5154

5255
Method %OnNew() As %Status
@@ -160,6 +163,75 @@ ClassMethod Configure() As %Boolean [ CodeMode = objectgenerator ]
160163
do %code.WriteLine(" quit 1")
161164
}
162165

166+
Method ConfigureNamespaceWebApplication()
167+
{
168+
Set root = ##class(%Library.File).NormalizeDirectory(##class(SourceControl.Git.Utils).TempFolder())
169+
Set deleteWebApp = ..HasNamespaceWebApp(.appDirectory) && '..namespaceLevelGitWebApp
170+
Set createWebApp = ..namespaceLevelGitWebApp && '..HasNamespaceWebApp()
171+
Do ..WebAppOperation("/git/"_$Namespace_"/", createWebApp, deleteWebApp, root)
172+
}
173+
174+
Method WebAppOperation(name, create As %Boolean, delete As %Boolean, root As %String) [ Private ]
175+
{
176+
Set namespace = $Namespace
177+
New $Namespace
178+
Set $Namespace = "%SYS"
179+
If $Extract(name) = "/" {
180+
Set name = $Extract(name,1,*-1)
181+
}
182+
If delete {
183+
If ##class(Security.Applications).Exists(name) {
184+
$$$ThrowOnError(##class(Security.Applications).Delete(name))
185+
Write !,"Removed web application "_name
186+
}
187+
Quit
188+
}
189+
190+
// These are the only things we want to coerce.
191+
Set props("AutheEnabled")=0 // No auth methods enabled = impossible to use
192+
Set props("InbndWebServicesEnabled")=0
193+
Set props("ServeFiles")=0
194+
Set props("Enabled")=1
195+
Set props("Name")=name
196+
Set props("NameSpace")=namespace
197+
Set props("Path")=root
198+
Set props("Type")=2
199+
Set props("Recurse")=1
200+
If create {
201+
Write !,"Creating web application: "_name_"... "
202+
$$$ThrowOnError(##class(Security.Applications).Create(name,.props))
203+
Write "done."
204+
} ElseIf ##class(Security.Applications).Exists(name) {
205+
Write !,"Web application '"_name_"' already exists."
206+
$$$ThrowOnError(##class(Security.Applications).Get(name,.existingProps))
207+
Set changes = 0
208+
Set key = ""
209+
For {
210+
Set key = $Order(props(key),1,value)
211+
Quit:key=""
212+
If (value '= $Get(existingProps(key))) {
213+
Write !,"Changing "_key_": "_$Get(existingProps(key))_" -> "_value
214+
Set changes = 1
215+
}
216+
}
217+
If changes {
218+
$$$ThrowOnError(##class(Security.Applications).Modify(name,.props))
219+
Write !,"Web application '"_name_"' updated."
220+
} Else {
221+
Write !,"No changes made to web application."
222+
}
223+
}
224+
}
225+
226+
ClassMethod HasNamespaceWebApp(Output webAppDirectory) As %Boolean
227+
{
228+
Set webAppDirectory = $System.CSP.GetFileName("/git/"_$Namespace_"/")
229+
If (webAppDirectory '= "") {
230+
Set webAppDirectory = ##class(%Library.File).NormalizeDirectory(webAppDirectory)
231+
}
232+
Quit (webAppDirectory '= "")
233+
}
234+
163235
Method OnAfterConfigure() As %Boolean
164236
{
165237
set defaultPromptFlag = $$$DisableBackupCharMask + $$$TrapCtrlCMask + $$$EnableQuitCharMask + $$$DisableHelpCharMask + $$$DisableHelpContextCharMask + $$$TrapErrorMask
@@ -186,6 +258,8 @@ Method OnAfterConfigure() As %Boolean
186258
}
187259
}
188260

261+
do ..ConfigureNamespaceWebApplication()
262+
189263
set gitDir = ##class(%File).NormalizeDirectory(..namespaceTemp)_".git"
190264
if '##class(%File).DirectoryExists(gitDir) {
191265
set list(1) = "Initialize empty repo"

cls/SourceControl/Git/StreamServer.cls

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Class SourceControl.Git.StreamServer Extends %CSP.StreamServer
77
ClassMethod OnPage() As %Status
88
{
99
if (%stream '= $$$NULLOREF) && $data(%base)#2 {
10-
set sourceControlInclude = ##class(SourceControl.Git.Utils).GetSourceControlInclude()
10+
set sourceControlInclude = ##class(SourceControl.Git.Utils).GetSourceControlInclude(%request.URLPrefix)
1111
while '%stream.AtEnd {
1212
set text = %stream.Read()
1313
set text = $replace(text,"{{baseHref}}",..EscapeHTML(%base))
@@ -19,4 +19,3 @@ ClassMethod OnPage() As %Status
1919
}
2020

2121
}
22-

cls/SourceControl/Git/Utils.cls

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,7 @@ ClassMethod FullExternalName(ByRef InternalName As %String, ByRef MappingExists
11161116
..TempFolder()_..ExternalName(.InternalName, .MappingExists)
11171117
}
11181118

1119-
ClassMethod NormalizeInternalName(ByRef name As %String) As %String
1119+
ClassMethod NormalizeInternalName(ByRef name As %String, Output fromWebApp As %Boolean = 0, Output cspFilename) As %String
11201120
{
11211121
//Studio passes name of routine with dots as it is in folders
11221122
//e.g. Package.SubPackage.Routine.mac has InternalName = /Package/SubPackage/Routine.mac
@@ -1132,6 +1132,16 @@ ClassMethod NormalizeInternalName(ByRef name As %String) As %String
11321132
if ($extract(name) '= "/") && (type'="csp") {
11331133
quit $piece(name,".",1,*-1)_"."_$zconvert($piece(name,".",*),"U")
11341134
}
1135+
1136+
if (name [ "/") && (type = "csp") {
1137+
set cspFilename = $System.CSP.GetFileName(name)
1138+
if (cspFilename '= "") && (cspFilename [ ..TempFolder()) {
1139+
set name = ..NameToInternalName(cspFilename)
1140+
set fromWebApp = 1
1141+
} else {
1142+
kill cspFilename
1143+
}
1144+
}
11351145

11361146
if (type = "inc") || (type = "mac") || (type = "int") {
11371147
set name = $extract($translate(name, "/", "."), 2, *)
@@ -1246,7 +1256,7 @@ ClassMethod FixProjectCspReferences(projectName As %String) As %Status
12461256

12471257
/// imports file if version in system is newer then version on disk.
12481258
/// if <var>force</var> = 1 then imports in any case.
1249-
ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose As %Boolean = 1) As %Status
1259+
ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose As %Boolean = 1, compile As %Boolean = 0) As %Status
12501260
{
12511261
#dim filename as %String = ..FullExternalName(.InternalName)
12521262
#dim fileTSH = ##class(%File).GetFileDateModified(filename)
@@ -1265,7 +1275,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A
12651275
if ($extract(InternalName, 1) = "/"){
12661276
set sc = ..ImportCSPFile(InternalName)
12671277
} else{
1268-
set sc = $system.OBJ.Load(filename,"-l-d")
1278+
set sc = $system.OBJ.Load(filename,$Select(compile:"ck-l",1:"-l-d"))
12691279
}
12701280
}
12711281
if sc {
@@ -2396,10 +2406,10 @@ ClassMethod GetPackageVersion() As %String [ CodeMode = objectgenerator ]
23962406
quit $$$OK
23972407
}
23982408

2399-
ClassMethod GetSourceControlInclude() As %String
2409+
ClassMethod GetSourceControlInclude(prefix As %String = "") As %String
24002410
{
24012411
quit $select(##class(%Library.EnsembleMgr).IsEnsembleInstalled():
2402-
"<script type=""text/javascript"" src=""/isc/studio/templates/ensemble/Ens_SourceControl.js""></script>",
2412+
"<script type=""text/javascript"" src="""_prefix_"/isc/studio/templates/ensemble/Ens_SourceControl.js""></script>",
24032413
1: "")
24042414
}
24052415

@@ -2615,4 +2625,3 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status
26152625
}
26162626

26172627
}
2618-

cls/SourceControl/Git/WebUIDriver.cls

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,65 @@ ClassMethod HandleRequest(pagePath As %String, InternalName As %String = "", Out
2727
if $isobject($get(responseJSON)) {
2828
do responseJSON.%ToJSON(%data)
2929
}
30+
} elseif pathStart = "vscode-workspace" {
31+
set handled = 1
32+
set namespace = $Namespace
33+
set instanceName = $ZConvert(##class(%SYS.System).InstanceGUID(),"L")
34+
set hasCSP = 0
35+
if context.IsInGitEnabledPackage && 'context.IsInDefaultPackage {
36+
set workspaceFilename = context.Package.Name_"-"_instanceName_".code-workspace"
37+
// Loading an IPM package in dev mode automatically creates a project for it, so filter to that.
38+
set filter = "?project="_context.Package.Name
39+
} else {
40+
set hasCSP = ##class(SourceControl.Git.Settings).HasNamespaceWebApp()
41+
set workspaceFilename = namespace_"-"_instanceName_".code-workspace"
42+
set filter = "?mapped=0"
43+
}
44+
set server = {
45+
"webServer": {
46+
"host": (%request.CgiEnvs("SERVER_NAME")),
47+
"port": (+%request.CgiEnvs("SERVER_PORT")),
48+
"scheme": ($Select(%request.Secure:"https",1:"http"))
49+
},
50+
"username": ($Username)
51+
}
52+
if (%request.URLPrefix '= "") {
53+
set server.webServer.pathPrefix = %request.URLPrefix
54+
}
55+
set workspaceDef = {
56+
"folders": [
57+
],
58+
"settings": {
59+
"intersystems.servers": {
60+
"/default": (instanceName)
61+
},
62+
"objectscript.conn": {
63+
"active": false
64+
}
65+
}
66+
}
67+
if hasCSP {
68+
do workspaceDef.folders.%Push({
69+
"name": ($Namespace),
70+
"uri": ("isfs://"_instanceName_":"_$Namespace_"/git/"_$Namespace_"?csp")
71+
})
72+
} else {
73+
do workspaceDef.folders.%Push({
74+
"name": ($Namespace),
75+
"uri": ("isfs://"_instanceName_":"_$Namespace_"/"_filter)
76+
})
77+
}
78+
set $Property(workspaceDef.settings."intersystems.servers",instanceName) = server
79+
set stream = ##class(%CSP.CharacterStream).%New()
80+
do stream.SetAttribute("ContentDisposition","attachment; filename="_workspaceFilename)
81+
set formatter = ##class(%JSON.Formatter).%New()
82+
do formatter.FormatToStream(workspaceDef,stream)
83+
$$$ThrowOnError(stream.%Save())
84+
do %request.Set("STREAMOID",##class(%CSP.Page).Encrypt(stream.%Oid()))
85+
set %request.Data("EXPIRES",1,"encrypt") = 1
86+
do %request.Set("EXPIRES",0)
87+
do ##class(%CSP.StreamServer).OnPreHTTP() // Need to call this to set headers properly
88+
set %stream = 1 // Leak this to webuidriver.csp
3089
} elseif $match(pathStart,"git-command|git|dirname|hostname|viewonly|contexts") {
3190
if (%request.Method = "GET") {
3291
set %data = ##class(%Stream.TmpCharacter).%New()

csp/sync.csp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@
146146

147147
&js<
148148
var outputContainer = document.getElementById('outputBox');
149-
var lineText = "#(escapedLine)#";
149+
var lineText = #(..QuoteJS(escapedLine))#;
150150
var lineTextNode = document.createTextNode(lineText);
151151
outputContainer.innerHTML += lineText + "<br>";
152152
>

git-webui/release/share/git-webui/webui/css/git-webui.css

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ body {
183183
padding-bottom: 100px;
184184
}
185185
#sidebar #sidebar-content > :first-child,
186-
#sidebar #sidebar-content > :nth-last-child(2) {
186+
#sidebar #sidebar-content > :nth-last-child(3) {
187187
border-top: 1px solid #5e5e5e;
188188
}
189189
#sidebar #sidebar-content h4:before {
@@ -216,26 +216,43 @@ body {
216216
#sidebar #sidebar-content #sidebar-tags h4:before {
217217
content: url(../img/tag.svg);
218218
}
219+
#sidebar #sidebar-content #sidebar-vscode a {
220+
color: white;
221+
}
222+
#sidebar #sidebar-content #sidebar-vscode h4 {
223+
padding: 0px;
224+
}
225+
#sidebar #sidebar-content #sidebar-vscode h4:before {
226+
content: url(../img/file.svg);
227+
}
228+
#sidebar #sidebar-content #sidebar-vscode {
229+
position: absolute;
230+
bottom: 80px;
231+
width: 16.7em;
232+
background-color: #333333;
233+
}
234+
#sidebar #sidebar-content #sidebar-context h4 {
235+
padding: 0px;
236+
}
219237
#sidebar #sidebar-content #sidebar-context h4:before {
220238
content: url(../img/context.svg);
221239
}
222240
#sidebar #sidebar-content #sidebar-context {
223241
position: absolute;
224-
bottom: 50px;
242+
bottom: 40px;
225243
width: 16.7em;
226-
padding-bottom: 0.3rem;
227-
margin-bottom: 0;
228244
background-color: #333333;
229245
}
246+
#sidebar #sidebar-content #sidebar-settings h4 {
247+
padding: 0px;
248+
}
230249
#sidebar #sidebar-content #sidebar-settings h4:before {
231250
content: url(../img/gear-fill.svg);
232251
}
233252
#sidebar #sidebar-content #sidebar-settings {
234253
position: absolute;
235254
bottom: 0px;
236255
width: 16.7em;
237-
padding-bottom: 0.3rem;
238-
margin-bottom: 0px;
239256
background-color: #333333;
240257
}
241258
#sidebar #sidebar-content ul {
Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)