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