diff --git a/.gitignore b/.gitignore
index a454f10a4..23b5b8d9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
.gradle
.idea
+.intellijPlatform
/build
/ide_for_launch
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 818777834..0ef3030b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,23 @@
All notable changes to the Zowe IntelliJ Plugin will be documented in this file.
+## [Unreleased]
+
+### Features
+
+* Feature: Correct alignment for fields in working set dialog ([745143cc](https://github.com/zowe/zowe-explorer-intellij/commit/745143cc))
+
+### Bufixes
+
+* Bugfix: Fixed unexpected freezes of IDE during work with USS part ([38f1f5b3](https://github.com/zowe/zowe-explorer-intellij/commit/38f1f5b3))
+* Bugfix: Fixed error that was caused by members renaming ([238ebc08](https://github.com/zowe/zowe-explorer-intellij/commit/238ebc08))
+* Bugfix: Fixed missed automatic translation to uppercase ([1b48de61](https://github.com/zowe/zowe-explorer-intellij/commit/1b48de61))
+* Bugfix: Fixed synchronization errors ([f96d9302](https://github.com/zowe/zowe-explorer-intellij/commit/f96d9302))
+* Bugfix: Fixed permissions change in opened file ([b04ed99e](https://github.com/zowe/zowe-explorer-intellij/commit/b04ed99e))
+* Bugfix: Fixed copy-paste folder to another folder with name conflicts ([13d7d773](https://github.com/zowe/zowe-explorer-intellij/commit/13d7d773))
+* Bugfix: Validation changes for LRECL field ([14e384a4](https://github.com/zowe/zowe-explorer-intellij/commit/14e384a4))
+* Bugfix: Fixed issue when job was not visible in JesExplorerView when Job Filter is created by JOBID ([5bb17263](https://github.com/zowe/zowe-explorer-intellij/commit/5bb17263))
+
## [1.2.2-221] (2024-08-21)
### Features
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 10c3dde15..a728a2cde 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -60,6 +60,6 @@ Consider some new feature for the plugin. The first that you need is to create a
- In case of bug - make a reproduction scheme or short description on how to achieve this;
- In case of new feature or improvement - describe, what you are trying to implement, how it should work, and (if applicable) why it should be introduced in the plugin;
6. After the changes are made, create a pull request on any of the main branches in the project repo. ***It is not a problem if you specified an incorrect target branch, we will help you with it before the changes are pushed***
-7. Assign Uladzislau Kalesnikau and Valiantsin Krus as reviewers to the pull request
+7. Assign Uladzislau Kalesnikau as a reviewer to the pull request
8. Attach the issue to the pull request
9. Wait on the approval (thanks in advance)
diff --git a/README.md b/README.md
index 2a8571341..0d269ede7 100644
--- a/README.md
+++ b/README.md
@@ -199,7 +199,6 @@ some of the communication chanels:
* [For Mainframe GitHub (create or review issues)](https://github.com/for-mainframe/For-Mainframe/issues)
* [Zowe Explorer IntelliJ GitHub (create or review issues)](https://github.com/zowe/zowe-explorer-intellij/issues)
* Email to: Uladzislau Kalesnikau (Team Lead of the IJMP)
-* Email to: Valiantsin Krus (Tech Lead of the IJMP)
**Note: GitHub issue is the preferred way of communicating in case of creating some bug/feature/request for enhancement.
If you need direct consulting or you have any related questions, please, reach us out using Slack channels or E-mail**
diff --git a/gradle.properties b/gradle.properties
index ec2259c5e..11419b630 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -14,7 +14,7 @@ org.gradle.jvmargs=-Xss1M
platformVersion = 2022.1
# SemVer format -> https://semver.org
-pluginVersion = 1.2.2-221
+pluginVersion = 1.2.3-221
pluginGroup = eu.ibagroup
pluginRepositoryUrl = https://github.com/for-mainframe/For-Mainframe
diff --git a/src/main/kotlin/eu/ibagroup/formainframe/config/connect/connectUtils.kt b/src/main/kotlin/eu/ibagroup/formainframe/config/connect/connectUtils.kt
index d10037724..2240ee40f 100644
--- a/src/main/kotlin/eu/ibagroup/formainframe/config/connect/connectUtils.kt
+++ b/src/main/kotlin/eu/ibagroup/formainframe/config/connect/connectUtils.kt
@@ -21,7 +21,7 @@ import eu.ibagroup.formainframe.ui.build.tso.config.TSOConfigWrapper
import eu.ibagroup.formainframe.ui.build.tso.ui.TSOSessionParams
import org.zowe.kotlinsdk.TsoData
-const val USER_OR_OWNER_SYMBOLS_MAX_SIZE: Int = 7
+const val USER_OR_OWNER_SYMBOLS_MAX_SIZE: Int = 8
/**
* Sends TSO request "oshell whoami", with which it receives the name of the real user (owner) of the system.
diff --git a/src/main/kotlin/eu/ibagroup/formainframe/config/ws/ui/AbstractWsDialog.kt b/src/main/kotlin/eu/ibagroup/formainframe/config/ws/ui/AbstractWsDialog.kt
index f8b7cc82f..035237d31 100644
--- a/src/main/kotlin/eu/ibagroup/formainframe/config/ws/ui/AbstractWsDialog.kt
+++ b/src/main/kotlin/eu/ibagroup/formainframe/config/ws/ui/AbstractWsDialog.kt
@@ -14,6 +14,7 @@ import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.SimpleListCellRenderer
+import com.intellij.ui.dsl.builder.RowLayout
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
@@ -102,7 +103,7 @@ abstract class AbstractWsDialog>(
+abstract class MFRemoteAttributesServiceBase>(
val dataOpsManager: DataOpsManager
) : AttributesService {
@@ -98,9 +97,7 @@ abstract class MFRemoteAttributesServiceBase Unit = {}
+
+ /**
+ * Static function is used to determine if Document exists for the Virtual File provided
+ * @param file - virtual file to check
+ * @return Document instance or null is no document exists for the given file
+ */
+ fun findDocumentForFile(file: VirtualFile): Document? {
+ return FileDocumentManager.getInstance().getDocument(file)
+ }
+
}
/**
diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt
index 67fef9a26..c4df54668 100644
--- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt
+++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt
@@ -17,6 +17,7 @@ import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
+import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent
import eu.ibagroup.formainframe.dataops.DataOpsManager
import eu.ibagroup.formainframe.dataops.attributes.FileAttributes
import eu.ibagroup.formainframe.dataops.attributes.RemoteUssAttributes
@@ -60,6 +61,14 @@ abstract class RemoteAttributedContentSynchronizer
events.filterIsInstance().forEach { event ->
fetchedAtLeastOnce.removeIf { it.file == event.file }
}
+
+ events.filterIsInstance().forEach { event ->
+ if (event.propertyName == VirtualFile.PROP_WRITABLE && !event.file.isDirectory) {
+ event.newValue.castOrNull()?.let {
+ DocumentedSyncProvider.findDocumentForFile(event.file)?.setReadOnly(!it)
+ }
+ }
+ }
}
}
)
diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/JobFetchProvider.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/JobFetchProvider.kt
index 6752542c7..40f0e9877 100644
--- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/JobFetchProvider.kt
+++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/JobFetchProvider.kt
@@ -70,6 +70,10 @@ class JobFetchProvider(dataOpsManager: DataOpsManager) :
val response = if (query.request.jobId.isNotEmpty()) {
api(query.connectionConfig).getFilteredJobs(
basicCredentials = query.connectionConfig.authToken,
+ // "owner=*" and "prefix=*" are needed to get the job by job ID.
+ // If we do not provide the values, the default ones will be used (the current user as owner)
+ owner = "*",
+ prefix = "*",
jobId = query.request.jobId,
execData = ExecData.YES
).cancelByIndicator(progressIndicator).execute()
diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/RemoteFileFetchProviderBase.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/RemoteFileFetchProviderBase.kt
index e8790ccf8..7fd877e89 100644
--- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/RemoteFileFetchProviderBase.kt
+++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/RemoteFileFetchProviderBase.kt
@@ -110,8 +110,10 @@ abstract class RemoteFileFetchProviderBase {
val fetched = fetchResponse(query, progressIndicator)
- return fetched.mapNotNull {
- convertResponseToFile(it)
+ return runWriteActionInEdtAndWait {
+ fetched.mapNotNull {
+ convertResponseToFile(it)
+ }
}
}
diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/RenameOperationRunner.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/RenameOperationRunner.kt
index 0a6ad8fbe..50c3f2e2e 100644
--- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/RenameOperationRunner.kt
+++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/RenameOperationRunner.kt
@@ -68,7 +68,7 @@ class RenameOperationRunner(private val dataOpsManager: DataOpsManager) : Operat
) {
when (val attributes = operation.attributes) {
is RemoteDatasetAttributes -> {
- attributes.requesters.map {
+ attributes.requesters.forEach {
try {
progressIndicator.checkCanceled()
val response = api(it.connectionConfig).renameDataset(
@@ -84,6 +84,7 @@ class RenameOperationRunner(private val dataOpsManager: DataOpsManager) : Operat
runWriteActionInEdtAndWait {
operation.file.rename(this, operation.newName)
}
+ return
} else {
throw CallException(response, "Unable to rename the selected dataset")
}
@@ -98,7 +99,7 @@ class RenameOperationRunner(private val dataOpsManager: DataOpsManager) : Operat
}
is RemoteMemberAttributes -> {
val parentAttributes = dataOpsManager.tryToGetAttributes(attributes.parentFile) as RemoteDatasetAttributes
- parentAttributes.requesters.map {
+ parentAttributes.requesters.forEach {
try {
progressIndicator.checkCanceled()
log.info("Checking for duplicate names in dataset ${parentAttributes.datasetInfo.name}")
@@ -117,7 +118,7 @@ class RenameOperationRunner(private val dataOpsManager: DataOpsManager) : Operat
).cancelByIndicator(progressIndicator).execute()
if (!response.isSuccessful) {
throw CallException(response, "Unable to duplicate the selected member")
- }
+ } else return
} else {
val response = api(it.connectionConfig).renameDatasetMember(
authorizationToken = it.connectionConfig.authToken,
@@ -134,6 +135,7 @@ class RenameOperationRunner(private val dataOpsManager: DataOpsManager) : Operat
runWriteActionInEdtAndWait {
operation.file.rename(this, operation.newName)
}
+ return
} else {
throw CallException(response, "Unable to rename the selected member")
}
@@ -149,7 +151,7 @@ class RenameOperationRunner(private val dataOpsManager: DataOpsManager) : Operat
}
is RemoteUssAttributes -> {
val parentDirPath = attributes.parentDirPath
- attributes.requesters.map {
+ attributes.requesters.forEach {
try {
progressIndicator.checkCanceled()
val response = api(it.connectionConfig).moveUssFile(
@@ -163,6 +165,7 @@ class RenameOperationRunner(private val dataOpsManager: DataOpsManager) : Operat
runWriteActionInEdtAndWait {
operation.file.rename(this, operation.newName)
}
+ return
} else {
throw CallException(response, "Unable to rename the selected file or directory")
}
diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/UssToUssFileMover.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/UssToUssFileMover.kt
index 4f989e067..817655b8c 100644
--- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/UssToUssFileMover.kt
+++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/UssToUssFileMover.kt
@@ -17,6 +17,8 @@ import eu.ibagroup.formainframe.dataops.DataOpsManager
import eu.ibagroup.formainframe.dataops.attributes.RemoteUssAttributes
import eu.ibagroup.formainframe.dataops.attributes.USS_DELIMITER
import eu.ibagroup.formainframe.dataops.exceptions.CallException
+import eu.ibagroup.formainframe.dataops.operations.DeleteOperation
+import eu.ibagroup.formainframe.dataops.operations.DeleteOperationRunner
import eu.ibagroup.formainframe.dataops.operations.OperationRunner
import eu.ibagroup.formainframe.dataops.operations.OperationRunnerFactory
import eu.ibagroup.formainframe.utils.cancelByIndicator
@@ -25,7 +27,6 @@ import eu.ibagroup.formainframe.utils.log
import org.zowe.kotlinsdk.CopyDataUSS
import org.zowe.kotlinsdk.DataAPI
import org.zowe.kotlinsdk.FilePath
-import org.zowe.kotlinsdk.MoveUssFile
import retrofit2.Call
import retrofit2.Response
@@ -43,6 +44,7 @@ class UssToUssFileMoverFactory : OperationRunnerFactory {
* Implements copying of uss file to uss directory inside 1 system
*/
class UssToUssFileMover(private val dataOpsManager: DataOpsManager) : AbstractFileMover() {
+
override fun canRun(operation: MoveCopyOperation): Boolean {
return operation.sourceAttributes is RemoteUssAttributes
&& operation.destinationAttributes is RemoteUssAttributes
@@ -57,30 +59,48 @@ class UssToUssFileMover(private val dataOpsManager: DataOpsManager) : AbstractFi
* Proceeds move/copy of uss file to uss directory
* @param connectionConfig connection configuration of system inside which to copy file
* @param operation requested operation
- * @param progressIndicator indicator that will show progress of copying/moving in UI
*/
private fun makeCall(
connectionConfig: ConnectionConfig,
- operation: MoveCopyOperation,
- progressIndicator: ProgressIndicator
- ): Triple, String, String> {
+ operation: MoveCopyOperation
+ ): Triple, () -> Unit>, String, String> {
val sourceAttributes = (operation.sourceAttributes as RemoteUssAttributes)
- val destinationAttributes = (operation.destinationAttributes as RemoteUssAttributes)
val from = sourceAttributes.path
- val to = destinationAttributes.path + USS_DELIMITER + (operation.newName ?: sourceAttributes.name)
- val api = api(connectionConfig)
- val call = if (operation.isMove) {
- api.moveUssFile(
- authorizationToken = connectionConfig.authToken,
- body = MoveUssFile(
- from = from
- ),
- filePath = FilePath(
- path = to
- )
- )
- } else {
- api.copyUssFile(
+ val to = computeUssDestination(operation)
+ val call = if (operation.isMove)
+ buildMoveCall(connectionConfig, operation, from, to)
+ else
+ buildCopyCall(connectionConfig, operation, from, to)
+ return Triple(call, from, to)
+ }
+
+ /**
+ * Function builds a Move call and Delete source callback after moving is performed
+ * @return Pair of Move call and its delete source file callback
+ */
+ private fun buildMoveCall(
+ connectionConfig: ConnectionConfig,
+ operation: MoveCopyOperation,
+ from: String,
+ to: String)
+ : Pair, () -> Unit> {
+ val copyCall = buildCopyCall(connectionConfig, operation, from, to).first
+ val deleteSourceCallback = buildDeleteSourceCallback(operation)
+ return Pair(copyCall, deleteSourceCallback)
+ }
+
+ /**
+ * Function builds a Copy call
+ * @return Pair of Copy call and empty callback function to execute after Copy is performed
+ */
+ private fun buildCopyCall(
+ connectionConfig: ConnectionConfig,
+ operation: MoveCopyOperation,
+ from: String,
+ to: String)
+ : Pair, () -> Unit> {
+ return Pair(
+ api(connectionConfig).copyUssFile(
authorizationToken = connectionConfig.authToken,
body = CopyDataUSS.CopyFromFileOrDir(
from = from,
@@ -93,8 +113,53 @@ class UssToUssFileMover(private val dataOpsManager: DataOpsManager) : AbstractFi
path = to
)
)
+ ) {}
+ }
+
+ /**
+ * Function builds a Delete callback which will be executed after successful Move is performed
+ * @return delete callback
+ */
+ private fun buildDeleteSourceCallback(operation: MoveCopyOperation): () -> Unit {
+ return {
+ val sourceAttributes = operation.sourceAttributes as RemoteUssAttributes
+ val deleteOperation = DeleteOperation(operation.source, sourceAttributes)
+ DeleteOperationRunner(dataOpsManager).run(deleteOperation)
+ }
+ }
+
+ /**
+ * Function is used to determine the correct USS destination for Move/Copy operation
+ * The possible list of destinations are:
+ *
+ * Copying:
+ * 1. Directory -> Directory without(with) conflict: Destination would be
+ * 2. Directory -> Directory with conflict, but "Use new name" option was pressed: Destination would be /
+ * 3. File -> Directory with conflict: Destination would be /
+ * 4. File -> Directory without conflict: Destination would be /
+ *
+ * Moving:
+ * 1. Directory -> Directory without(with) conflict: Destination would be
+ * 2. Directory -> Directory with conflict and "Use new name" is pressed: Destination would be /
+ * 3. File -> Directory the same behavior as for Copying
+ *
+ * * example: mv(cp) -Rf /u//test /u//destination
+ * * (if test is present under destination all files and subdirs from /u//test will be copied/moved
+ * * and overwritten in /u//destination/test, otherwise test would be copied/moved to /u//destination/test)
+ * * If operation is Move, the source would be deleted afterward
+ *
+ * @return target destination in String format
+ */
+ private fun computeUssDestination(operation: MoveCopyOperation) : String {
+ val destinationRootPath = (operation.destinationAttributes as RemoteUssAttributes).path
+ val destinationNewName = operation.newName
+ val destinationNewNameWithDelimiter = USS_DELIMITER + operation.newName
+ // Copying or Moving USS directory
+ return if (operation.source.isDirectory && operation.destination.isDirectory) {
+ destinationRootPath + if (destinationNewName != null) destinationNewNameWithDelimiter else ""
}
- return Triple(call.cancelByIndicator(progressIndicator), from, to)
+ // Copying or Moving USS file
+ else destinationRootPath + USS_DELIMITER + (operation.newName ?: operation.sourceAttributes?.name)
}
/**
@@ -106,11 +171,14 @@ class UssToUssFileMover(private val dataOpsManager: DataOpsManager) : AbstractFi
var throwable: Throwable? = null
for ((requester, _) in operation.commonUrls(dataOpsManager)) {
try {
- val (call, from, to) = makeCall(requester.connectionConfig, operation, progressIndicator)
+ val (call, from, to) = makeCall(requester.connectionConfig, operation)
val operationName = if (operation.isMove) "move" else "copy"
- val response: Response = call.execute()
+ val response: Response = call.first.cancelByIndicator(progressIndicator).execute()
if (!response.isSuccessful) {
throwable = CallException(response, "Cannot $operationName $from to $to")
+ } else {
+ // Call the built early callback for source file/dir deletion (always empty callback for Copy)
+ call.second.invoke()
}
break
} catch (t: Throwable) {
diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/AllocationDialog.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/AllocationDialog.kt
index 337f21312..2fdaf76fd 100644
--- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/AllocationDialog.kt
+++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/AllocationDialog.kt
@@ -54,12 +54,12 @@ class AllocationDialog(project: Project?, config: ConnectionConfig, override var
private lateinit var advancedParametersField: JTextField
private lateinit var presetsBox: JComboBox
private val HLQ = getUsername(config)
+ private val nonNegativeIntRange = IntRange(0, Int.MAX_VALUE - 1)
+ private val positiveIntRange = IntRange(1, Int.MAX_VALUE - 1)
private val mainPanel by lazy {
val sameWidthLabelsGroup = "ALLOCATION_DIALOG_LABELS_WIDTH_GROUP"
val sameWidthComboBoxGroup = "ALLOCATION_DIALOG_COMBO_BOX_WIDTH_GROUP"
- val nonNegativeIntRange = IntRange(0, Int.MAX_VALUE - 1)
- val positiveIntRange = IntRange(1, Int.MAX_VALUE - 1)
panel {
row {
@@ -199,7 +199,7 @@ class AllocationDialog(project: Project?, config: ConnectionConfig, override var
row {
label("Record Length: ")
.widthGroup(sameWidthLabelsGroup)
- intTextField(positiveIntRange)
+ intTextField(nonNegativeIntRange)
.bindText(
{ state.allocationParameters.recordLength?.toString() ?: "0" },
{ state.allocationParameters.recordLength = it.toIntOrNull() }
@@ -334,6 +334,7 @@ class AllocationDialog(project: Project?, config: ConnectionConfig, override var
return validateDatasetNameOnInput(datasetNameField)
?: validateForBlank(memberNameField)
?: validateMemberName(memberNameField)
+ ?: validateLrecl(recordFormatBox, recordLengthField)
?: defaultValidationInfos.firstOrNull()
?: validateVolser(advancedParametersField)
}
@@ -342,6 +343,26 @@ class AllocationDialog(project: Project?, config: ConnectionConfig, override var
return mainPanel.preferredFocusedComponent ?: super.getPreferredFocusedComponent()
}
+ /**
+ * Function for validating LRECL value
+ * @param recordFormatBox RecordFormat combo box
+ * @param recordLengthField record length text field
+ * @return ValidationInfo in case of error or null otherwise
+ */
+ private fun validateLrecl(recordFormatBox: JComboBox, recordLengthField: JTextField): ValidationInfo? {
+ val range = if (recordFormatBox.selectedItem == RecordFormat.U)
+ nonNegativeIntRange
+ else
+ positiveIntRange
+ return if (recordLengthField.text.toIntOrNull() !in range)
+ ValidationInfo(
+ "Please enter a number from ${range.first} to ${range.last}",
+ recordLengthField
+ )
+ else
+ null
+ }
+
init {
title = "Allocate Dataset"
init()
diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt
index 56e00d3ae..70434ddbd 100644
--- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt
+++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt
@@ -33,6 +33,7 @@ import eu.ibagroup.formainframe.dataops.DataOpsManager
import eu.ibagroup.formainframe.dataops.Query
import eu.ibagroup.formainframe.dataops.attributes.AttributesService
import eu.ibagroup.formainframe.dataops.attributes.FileAttributes
+import eu.ibagroup.formainframe.dataops.attributes.RemoteUssAttributes
import eu.ibagroup.formainframe.dataops.attributes.attributesListener
import eu.ibagroup.formainframe.dataops.content.synchronizer.DocumentedSyncProvider
import eu.ibagroup.formainframe.dataops.content.synchronizer.SaveStrategy
@@ -42,6 +43,7 @@ import eu.ibagroup.formainframe.explorer.*
import eu.ibagroup.formainframe.utils.*
import eu.ibagroup.formainframe.utils.crudable.EntityWithUuid
import eu.ibagroup.formainframe.vfs.MFBulkFileListener
+import eu.ibagroup.formainframe.vfs.MFVirtualFile
import eu.ibagroup.formainframe.vfs.MFVirtualFileSystem
import org.jetbrains.concurrency.AsyncPromise
import java.awt.Component
@@ -53,6 +55,8 @@ import javax.swing.tree.TreeSelectionModel
val EXPLORER_VIEW = DataKey.create>("explorerView")
+private val log = log>()
+
fun > AnActionEvent.getExplorerView(clazz: Class): ExplorerView? {
return getData(EXPLORER_VIEW).castOrNull(clazz)
}
@@ -221,6 +225,18 @@ abstract class ExplorerTreeView().filter {
+ it.propertyName == VirtualFile.PROP_NAME && it.file is MFVirtualFile
+ }.forEach {
+ (it.newValue as? String)?.let { newName ->
+ updateAttributesForChildrenInEditor(it.file, newName)
+ }
+ }
+ }
}
},
disposable = this
@@ -406,4 +422,45 @@ abstract class ExplorerTreeView
+ if (VfsUtilCore.isAncestor(renamedFile, openFile, false)) {
+ val oldAttributes = dataOpsManager.tryToGetAttributes(openFile)
+ if (parentAttributes is RemoteUssAttributes && oldAttributes is RemoteUssAttributes) {
+ val relativePathToFile = oldAttributes.path.removePrefix(parentAttributes.path)
+ val newPath = "/${parentAttributes.parentDirPath}/$newName$relativePathToFile"
+ val newAttributes = RemoteUssAttributes(
+ newPath,
+ oldAttributes.isDirectory,
+ oldAttributes.fileMode,
+ oldAttributes.url,
+ oldAttributes.requesters,
+ oldAttributes.length,
+ oldAttributes.uid,
+ oldAttributes.owner,
+ oldAttributes.gid,
+ oldAttributes.groupId,
+ oldAttributes.modificationTime,
+ oldAttributes.symlinkTarget
+ )
+ log.info(
+ "Update attributes for file in editor.\nVirtual file - $openFile.\n" +
+ "Old attributes - $oldAttributes.\nNew attributes - $newAttributes."
+ )
+ val attributesService =
+ dataOpsManager.getAttributesService(oldAttributes::class.java, renamedFile::class.java)
+ attributesService.updateAttributes(oldAttributes, newAttributes)
+ }
+ }
+ }
+ }
+
}
diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/RenameDialog.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/RenameDialog.kt
index cbb6e929e..ead5971a9 100644
--- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/RenameDialog.kt
+++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/RenameDialog.kt
@@ -61,7 +61,8 @@ class RenameDialog(
textField()
.bindText(this@RenameDialog::state)
.validationOnApply { validateForBlank(it) ?: validateOnInput(it) }
- .apply { focused() }
+ .onApply { state = state.uppercaseIfNeeded() }
+ .focused()
}
}
}
@@ -78,6 +79,8 @@ class RenameDialog(
* Validate a new name for the selected node component
*/
private fun validateOnInput(component: JTextField): ValidationInfo? {
+ component.text = component.text.uppercaseIfNeeded()
+
val attributes = selectedNodeData.attributes
validateForTheSameValue(attributes?.name, component)?.let { return it }
@@ -102,4 +105,15 @@ class RenameDialog(
return null
}
+ /**
+ * Convert the string to upper case if partitioned dataset, sequential dataset or dataset member is selected
+ */
+ private fun String.uppercaseIfNeeded(): String {
+ return if (node is LibraryNode || node is FileLikeDatasetNode) {
+ this.uppercase()
+ } else {
+ this
+ }
+ }
+
}
diff --git a/src/main/kotlin/eu/ibagroup/formainframe/utils/openapiUtils.kt b/src/main/kotlin/eu/ibagroup/formainframe/utils/openapiUtils.kt
index 742ec3135..5d22d023d 100644
--- a/src/main/kotlin/eu/ibagroup/formainframe/utils/openapiUtils.kt
+++ b/src/main/kotlin/eu/ibagroup/formainframe/utils/openapiUtils.kt
@@ -11,7 +11,10 @@
package eu.ibagroup.formainframe.utils
import com.intellij.openapi.Disposable
-import com.intellij.openapi.application.*
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.PathManager
+import com.intellij.openapi.application.runInEdt
+import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.components.ComponentManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
@@ -187,10 +190,12 @@ inline fun runWriteActionInEdt(crossinline block: () -> Unit) {
}
}
-inline fun runWriteActionInEdtAndWait(crossinline block: () -> Unit) {
+inline fun runWriteActionInEdtAndWait(crossinline block: () -> T): T {
+ var result: T? = null
runInEdtAndWait {
- runWriteAction(block)
+ result = runWriteAction(block)
}
+ return result ?: throw Exception("runWriteAction did not return any result")
}
/** Return the specified logger instance */
diff --git a/src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeViewTestSpec.kt b/src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeViewTestSpec.kt
index 207b3fca3..0ce52db08 100644
--- a/src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeViewTestSpec.kt
+++ b/src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeViewTestSpec.kt
@@ -16,11 +16,15 @@ import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import eu.ibagroup.formainframe.config.connect.ConnectionConfig
import eu.ibagroup.formainframe.dataops.DataOpsManager
+import eu.ibagroup.formainframe.dataops.attributes.AttributesService
+import eu.ibagroup.formainframe.dataops.attributes.FileAttributes
+import eu.ibagroup.formainframe.dataops.attributes.RemoteUssAttributes
import eu.ibagroup.formainframe.dataops.content.synchronizer.ContentSynchronizer
import eu.ibagroup.formainframe.explorer.*
import eu.ibagroup.formainframe.testutils.WithApplicationShouldSpec
import eu.ibagroup.formainframe.testutils.testServiceImpl.TestDataOpsManagerImpl
import eu.ibagroup.formainframe.utils.service
+import eu.ibagroup.formainframe.vfs.MFVirtualFile
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.shouldBe
import io.mockk.*
@@ -33,6 +37,7 @@ class ExplorerTreeViewTestSpec: WithApplicationShouldSpec({
context("Explorer module: ui/ExplorerTreeView") {
lateinit var fileExplorerView: ExplorerTreeView<*, *, *>
+ lateinit var attributesServiceMock: AttributesService
val explorerMock = mockk>()
every { explorerMock.componentManager } returns ApplicationManager.getApplication()
@@ -80,7 +85,24 @@ class ExplorerTreeViewTestSpec: WithApplicationShouldSpec({
override fun getContentSynchronizer(file: VirtualFile): ContentSynchronizer {
return contentSynchronizerMock
}
+
+ override fun tryToGetAttributes(file: VirtualFile): FileAttributes? {
+ return mockk()
+ }
}
+ mockkObject(dataOpsManagerService.testInstance)
+
+ attributesServiceMock = mockk()
+ every {
+ attributesServiceMock.updateAttributes(any(), any())
+ } returns Unit
+
+ every {
+ dataOpsManagerService.testInstance.getAttributesService(
+ RemoteUssAttributes::class.java,
+ MFVirtualFile::class.java
+ )
+ } returns attributesServiceMock
}
afterEach {
@@ -100,5 +122,61 @@ class ExplorerTreeViewTestSpec: WithApplicationShouldSpec({
assertSoftly { closedFileSize shouldBe 0 }
}
+ // updateAttributesForChildrenInEditor
+ should("update attributes for files in editor if renamed file is their ancestor") {
+ var numOfCalls = 0
+ every { dataOpsManagerService.testInstance.tryToGetAttributes(any()) } answers {
+ numOfCalls++
+ if (numOfCalls == 1) {
+ mockk {
+ every { path } returns "/u/USER/dir/"
+ every { parentDirPath } returns "/u/USER"
+ }
+ } else {
+ RemoteUssAttributes(
+ "/u/USER/dir/file.txt",
+ false,
+ mockk(),
+ "https://hostname:port",
+ mutableListOf()
+ )
+ }
+ }
+
+ fileExplorerView.updateAttributesForChildrenInEditor(mockk(), "newDir")
+
+ verify { attributesServiceMock.updateAttributes(any(), any()) }
+ }
+ should("don't update attributes for files in editor if renamed file is not their ancestor") {
+ every { VfsUtilCore.isAncestor(any(), any(), any()) } returns false
+
+ fileExplorerView.updateAttributesForChildrenInEditor(mockk(), "newDir")
+
+ verify(exactly = 0) {
+ attributesServiceMock.updateAttributes(any(), any())
+ }
+ }
+ should("don't update attributes for files in editor if attributes are not USS attributes") {
+
+ fileExplorerView.updateAttributesForChildrenInEditor(mockk(), "newDir")
+
+ verify(exactly = 0) {
+ attributesServiceMock.updateAttributes(any(), any())
+ }
+ }
+ should("don't update attributes for files in editor if old attributes are not USS attributes") {
+ var numOfCalls = 0
+ every { dataOpsManagerService.testInstance.tryToGetAttributes(any()) } answers {
+ numOfCalls++
+ if (numOfCalls == 1) mockk()
+ else mockk()
+ }
+
+ fileExplorerView.updateAttributesForChildrenInEditor(mockk(), "newDir")
+
+ verify(exactly = 0) {
+ attributesServiceMock.updateAttributes(any(), any())
+ }
+ }
}
})
\ No newline at end of file