From eef20ad857ee27ae68009336e90d28813950ea55 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Sat, 27 Mar 2021 01:06:56 -0400 Subject: [PATCH 1/2] Add support for project pinning --- res/pin_16px.xcf | Bin 0 -> 998 bytes .../kotlin/controller/ProjectController.kt | 66 ++++++++++++++++-- src/main/kotlin/controller/SlackController.kt | 2 +- src/main/kotlin/io/SettingsManager.kt | 66 +++++++++++++++--- src/main/kotlin/io/SettingsMigrator.kt | 10 +-- src/main/kotlin/model/Project.kt | 15 ++-- src/main/kotlin/model/settings/Default.kt | 8 ++- src/main/kotlin/model/settings/Migration.kt | 25 ++++++- src/main/kotlin/model/settings/v2/Settings.kt | 11 +++ .../kotlin/model/settings/v2/SlackConfig.kt | 5 ++ .../ui/fragment/ProjectListCellFragment.kt | 22 +++++- src/main/kotlin/ui/style/Images.kt | 1 + src/main/resources/Pin.png | Bin 0 -> 795 bytes .../controller/ProjectControllerTest.kt | 62 +++++++++++++++- src/test/kotlin/io/SettingsManagerTest.kt | 19 +++++ 15 files changed, 277 insertions(+), 35 deletions(-) create mode 100644 res/pin_16px.xcf create mode 100644 src/main/kotlin/model/settings/v2/Settings.kt create mode 100644 src/main/kotlin/model/settings/v2/SlackConfig.kt create mode 100644 src/main/resources/Pin.png diff --git a/res/pin_16px.xcf b/res/pin_16px.xcf new file mode 100644 index 0000000000000000000000000000000000000000..8658f7f82e5fec8ad18872d64cb28edd2ca02c18 GIT binary patch literal 998 zcmd6m&2G~`5XUEu6T4{~O8HPI92N@N^k6CB1(4_gaX{h#58ybCW0BaE?b7zv7eI&$ zuYlB-sH#dxyg;O=QcryWPE?}tFtf3xMdAUN)qm%=Gvi(F+8ai*J#pxd#mD|&0Aw4b zn7@*4lWH~U?vU(V3bDxUl1$Pq(*2UT?jGqU$q8v{*H31%AkJwFoZQ?^11}Fo;vmW= z;??UnZ#lD}N~4(<25y)}BaU(PNiWNfrh&+kX*Akt^~Rx}Op{dflylQ?=!w2+gB?l_ z|B)UD)i`~a?RkC_hvGR6j`Tsv?tUEQB8vSjIe4P+BaK@c3ys$`Zfopl z%-@sOtIQ(cwR!$$C6j}v%t%IlTjT1#sP;rDHX~bk)mKNs^O6~#S6=m1j^p2|_ya&` zgC#Anl^W2JCalSZigDbMP0Df10z)oA$`<^VpghVg_)lhNJFxupz1xPHGra0raN|1g z-oWCj+aSh^p^3$hwh4uE*}&rbOB0IvqK^2^W-t(kHe7e+xLDwY(}Z7y^LqUfU6XF< W^(oFfbn{#bmJd%m4q&ZA+JZlcc&kJJ literal 0 HcmV?d00001 diff --git a/src/main/kotlin/controller/ProjectController.kt b/src/main/kotlin/controller/ProjectController.kt index 0ffb4e9..b0dd78e 100644 --- a/src/main/kotlin/controller/ProjectController.kt +++ b/src/main/kotlin/controller/ProjectController.kt @@ -2,28 +2,82 @@ package edu.erittenhouse.gitlabtimetracker.controller import edu.erittenhouse.gitlabtimetracker.controller.result.ProjectFetchResult import edu.erittenhouse.gitlabtimetracker.gitlab.GitlabAPI +import edu.erittenhouse.gitlabtimetracker.io.SettingsManager import edu.erittenhouse.gitlabtimetracker.model.Project +import edu.erittenhouse.gitlabtimetracker.ui.util.suspension.SuspendingController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.javafx.JavaFx import kotlinx.coroutines.withContext -import tornadofx.Controller -import tornadofx.asObservable +import tornadofx.* -class ProjectController : Controller() { +class ProjectController : SuspendingController() { private val gitlabAPI by inject() private val credentialController by inject() + private val settings = SettingsManager(find().fileLocation) val projects = mutableListOf().asObservable() + private var pinnedProjects = listOf() + private var unpinnedProjects = listOf() + /** - * Fetches user's projects from GitLab and + * Fetches user's projects from GitLab and adds them to the projects list */ suspend fun fetchProjects(): ProjectFetchResult { val credentials = credentialController.credentials ?: return ProjectFetchResult.NoCredentials - val fullProjectList = gitlabAPI.project.listUserMemberProjects(credentials).map { Project.fromGitlabDto(it) } + val pinnedProjectIDs = settings.getPinnedProjects() + val fullProjectList = gitlabAPI.project.listUserMemberProjects(credentials).map { Project.fromGitlabDto(it, isPinned = it.id in pinnedProjectIDs) } + val (localPinnedProjects, localUnpinnedProjects) = fullProjectList.partition { it.pinned } + pinnedProjects = localPinnedProjects + unpinnedProjects = localUnpinnedProjects withContext(Dispatchers.JavaFx) { - projects.setAll(fullProjectList) + projects.setAll(localPinnedProjects + localUnpinnedProjects) } return ProjectFetchResult.ProjectsRetrieved } + + /** + * Marks a project as pinned and moves it to the top of the project list + */ + suspend fun pinProject(projectID: Int) { + val localPinnedProjects = pinnedProjects.toMutableList() + val localUnpinnedProjects = unpinnedProjects.toMutableList() + + val projectToPinIdx = localUnpinnedProjects.indexOfFirst { it.id == projectID } + // If we can't find the project in question just return + if (projectToPinIdx == -1) return + + localPinnedProjects.add(localUnpinnedProjects.removeAt(projectToPinIdx).copy(pinned = true)) + + val currentPinnedProjectIDs = settings.getPinnedProjects() + projectID + settings.setPinnedProjects(currentPinnedProjectIDs) + pinnedProjects = localPinnedProjects + unpinnedProjects = localUnpinnedProjects + + withContext(Dispatchers.JavaFx) { + projects.setAll(localPinnedProjects + localUnpinnedProjects) + } + } + + /** + * Marks a project as unpinned and moves it immediately after the project list + */ + suspend fun unpinProject(projectID: Int) { + val localPinnedProjects = pinnedProjects.toMutableList() + val localUnpinnedProjects = unpinnedProjects.toMutableList() + + val projectToUnpinIdx = localPinnedProjects.indexOfFirst { it.id == projectID } + if (projectToUnpinIdx == -1) return + + localUnpinnedProjects.add(0, localPinnedProjects.removeAt(projectToUnpinIdx).copy(pinned = false)) + + val currentPinnedProjectIDs = settings.getPinnedProjects() - projectID + settings.setPinnedProjects(currentPinnedProjectIDs) + pinnedProjects = localPinnedProjects + unpinnedProjects = localUnpinnedProjects + + withContext(Dispatchers.JavaFx) { + projects.setAll(localPinnedProjects + localUnpinnedProjects) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/controller/SlackController.kt b/src/main/kotlin/controller/SlackController.kt index 7a0effb..8e00803 100644 --- a/src/main/kotlin/controller/SlackController.kt +++ b/src/main/kotlin/controller/SlackController.kt @@ -5,7 +5,7 @@ import edu.erittenhouse.gitlabtimetracker.controller.result.SlackLoginResult import edu.erittenhouse.gitlabtimetracker.io.SettingsManager import edu.erittenhouse.gitlabtimetracker.model.Issue import edu.erittenhouse.gitlabtimetracker.model.SlackCredential -import edu.erittenhouse.gitlabtimetracker.model.settings.v1.SlackConfig +import edu.erittenhouse.gitlabtimetracker.model.settings.v2.SlackConfig import edu.erittenhouse.gitlabtimetracker.slack.SlackAPI import edu.erittenhouse.gitlabtimetracker.slack.result.LoginResult import edu.erittenhouse.gitlabtimetracker.ui.util.suspension.SuspendingController diff --git a/src/main/kotlin/io/SettingsManager.kt b/src/main/kotlin/io/SettingsManager.kt index 2be0967..08921a8 100644 --- a/src/main/kotlin/io/SettingsManager.kt +++ b/src/main/kotlin/io/SettingsManager.kt @@ -3,9 +3,9 @@ package edu.erittenhouse.gitlabtimetracker.io import com.fasterxml.jackson.module.kotlin.readValue import edu.erittenhouse.gitlabtimetracker.io.error.SettingsErrors import edu.erittenhouse.gitlabtimetracker.model.GitlabCredential +import edu.erittenhouse.gitlabtimetracker.model.settings.NewestSettings +import edu.erittenhouse.gitlabtimetracker.model.settings.NewestSlackConfig import edu.erittenhouse.gitlabtimetracker.model.settings.defaultSettings -import edu.erittenhouse.gitlabtimetracker.model.settings.v1.Settings -import edu.erittenhouse.gitlabtimetracker.model.settings.v1.SlackConfig import edu.erittenhouse.gitlabtimetracker.util.JsonMapper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex @@ -13,11 +13,16 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.io.File +/** + * The settings manager provides an interface for interacting with the settings file in the user's home directory. + * All functions here should be completely main-thread-safe, as they switch to the IO dispatcher whenever file interaction + * is required. + */ class SettingsManager(private val fileLocation: String = System.getProperty("user.home") + "/.gtt") { private companion object FileAccess { private val fileMutexes = mutableMapOf() private val mutexMapMutex = Mutex() - private val statesByFile = mutableMapOf() + private val statesByFile = mutableMapOf() private suspend inline fun lockingSettingsForFile(fileName: String, action: () -> T): T { val mutexForFile = mutexMapMutex.withLock { @@ -27,13 +32,22 @@ class SettingsManager(private val fileLocation: String = System.getProperty("use return mutexForFile.withLock(action = action) } + /** + * Clears the in-memory cache for the given file + */ + suspend fun clearCache(fileName: String) { + lockingSettingsForFile(fileName) { + statesByFile.remove(fileName) + } + } + /** * Fetches the cached settings from memory, falling back on and caching the value from * disk if not in memory, finally returning null if the settings cannot be retrieved * * @throws SettingsErrors.DiskIOError if the data exists on disk but couldn't be read */ - suspend fun fetchSettings(fileName: String): Settings? { + suspend fun fetchSettings(fileName: String): NewestSettings? { return lockingSettingsForFile(fileName) { if (statesByFile[fileName] != null) return statesByFile[fileName] @@ -42,7 +56,7 @@ class SettingsManager(private val fileLocation: String = System.getProperty("use if (!settingsFile.exists()) return@withContext null val settings = try { - JsonMapper.readValue(settingsFile) + JsonMapper.readValue(settingsFile) } catch (e: Exception) { throw SettingsErrors.DiskIOError(fileName, "Failed to retrieve saved settings.", e) } @@ -58,7 +72,7 @@ class SettingsManager(private val fileLocation: String = System.getProperty("use * * @throws SettingsErrors.DiskIOError if persisting the settings to disk fails */ - suspend fun saveSettings(fileName: String, settings: Settings): Unit = withContext(Dispatchers.IO) { + suspend fun saveSettings(fileName: String, settings: NewestSettings): Unit = withContext(Dispatchers.IO) { lockingSettingsForFile(fileName) { statesByFile[fileName] = settings @@ -74,6 +88,12 @@ class SettingsManager(private val fileLocation: String = System.getProperty("use } } + /** + * Clears the in-memory cache for this settings manager. + */ + suspend fun clearCache() { + FileAccess.clearCache(fileLocation) + } /** * Saves gitlab credentials to disk. * @@ -81,7 +101,15 @@ class SettingsManager(private val fileLocation: String = System.getProperty("use */ suspend fun setCredential(credential: GitlabCredential) { val currentSettings = fetchSettings(fileLocation) - val newSettings = currentSettings?.copy(gitlabCredentials = credential) ?: defaultSettings(credential) + + // If the base URL on the credential changed, we should clear out the list of pinned projects + val pinnedProjectIDList = if (currentSettings?.gitlabCredentials?.gitlabBaseURL != credential.gitlabBaseURL) { + emptySet() + } else { + currentSettings.pinnedProjectIDs + } + + val newSettings = currentSettings?.copy(gitlabCredentials = credential, pinnedProjectIDs = pinnedProjectIDList) ?: defaultSettings(credential) saveSettings(fileLocation, newSettings) } @@ -91,12 +119,24 @@ class SettingsManager(private val fileLocation: String = System.getProperty("use * @throws SettingsErrors.DiskIOError if the disk operation fails * @throws SettingsErrors.RequiredMissingError if required settings have not yet been set, such as gitlab credentials */ - suspend fun setSlackConfig(slackEnabled: Boolean, credential: SlackConfig? = null) { + suspend fun setSlackConfig(slackEnabled: Boolean, credential: NewestSlackConfig? = null) { val currentSettings = fetchSettings(fileLocation) ?: throw SettingsErrors.RequiredMissingError() val newSettings = currentSettings.copy(slackConfig = credential ?: currentSettings.slackConfig, slackEnabled = slackEnabled) saveSettings(fileLocation, newSettings) } + /** + * Saves the current list of pinned projects to disk. + * + * @throws SettingsErrors.DiskIOError if the disk write operation fails + * @throws SettingsErrors.RequiredMissingError if the required settings have not yet been set, such as gitlab credentials + */ + suspend fun setPinnedProjects(pinnedProjects: Set) { + val currentSettings = fetchSettings(fileLocation) ?: throw SettingsErrors.RequiredMissingError() + val newSettings = currentSettings.copy(pinnedProjectIDs = pinnedProjects) + saveSettings(fileLocation, newSettings) + } + /** * Tries to retrieve gitlab credentials from disk, or in-memory cache if applicable * @@ -111,7 +151,7 @@ class SettingsManager(private val fileLocation: String = System.getProperty("use * @return Slack credentials from disk or null if user did not sign in with slack * @throws SettingsErrors.DiskIOError if data exists on disk but couldn't be read */ - suspend fun getSlackConfig(): SlackConfig? = fetchSettings(fileLocation)?.slackConfig + suspend fun getSlackConfig(): NewestSlackConfig? = fetchSettings(fileLocation)?.slackConfig /** * Tries to retrieve whether or not slack integration is enabled. @@ -119,4 +159,12 @@ class SettingsManager(private val fileLocation: String = System.getProperty("use * @throws SettingsErrors.DiskIOError if data exists on disk but couldn't be read */ suspend fun getSlackEnabled(): Boolean = fetchSettings(fileLocation)?.slackEnabled ?: false + + /** + * Tries to retrieve the list of pinned projects. + * + * @return The list of project IDs that are pinned, if any + * @throws SettingsErrors.DiskIOError if data exists on the disk but couldn't be read + */ + suspend fun getPinnedProjects(): Set = fetchSettings(fileLocation)?.pinnedProjectIDs ?: emptySet() } \ No newline at end of file diff --git a/src/main/kotlin/io/SettingsMigrator.kt b/src/main/kotlin/io/SettingsMigrator.kt index a63907b..de1826e 100644 --- a/src/main/kotlin/io/SettingsMigrator.kt +++ b/src/main/kotlin/io/SettingsMigrator.kt @@ -4,11 +4,7 @@ import com.fasterxml.jackson.module.kotlin.readValue import edu.erittenhouse.gitlabtimetracker.io.error.SettingsErrors import edu.erittenhouse.gitlabtimetracker.io.result.FileMigrationResult import edu.erittenhouse.gitlabtimetracker.io.result.MigrationResult -import edu.erittenhouse.gitlabtimetracker.model.settings.VersionedSettings -import edu.erittenhouse.gitlabtimetracker.model.settings.newestMigrationVersion -import edu.erittenhouse.gitlabtimetracker.model.settings.settingsMigrations -import edu.erittenhouse.gitlabtimetracker.model.settings.v1.Settings -import edu.erittenhouse.gitlabtimetracker.model.settings.versionToSettings +import edu.erittenhouse.gitlabtimetracker.model.settings.* import edu.erittenhouse.gitlabtimetracker.util.JsonMapper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -52,7 +48,7 @@ suspend fun migrateSettingsFile(fileLocation: String): FileMigrationResult = wit * * @throws SettingsErrors.ParseError if the settings could not be parsed from the file content */ -fun migrateSettings(fileContent: ByteArray): MigrationResult { +fun migrateSettings(fileContent: ByteArray): MigrationResult { val versionedSettings = try { JsonMapper.readValue(fileContent) } catch(e: Exception) { @@ -83,7 +79,7 @@ fun migrateSettings(fileContent: ByteArray): MigrationResult { } // Verify we got the correct settings type - val convertedSettings = currentSettings as? Settings + val convertedSettings = currentSettings as? NewestSettings ?: return MigrationResult.MigrationProducedUnexpectedModel(currentSettings.version) return MigrationResult.MigrationSucceeded(convertedSettings) } diff --git a/src/main/kotlin/model/Project.kt b/src/main/kotlin/model/Project.kt index 7915d1f..11312e8 100644 --- a/src/main/kotlin/model/Project.kt +++ b/src/main/kotlin/model/Project.kt @@ -3,14 +3,15 @@ package edu.erittenhouse.gitlabtimetracker.model import edu.erittenhouse.gitlabtimetracker.gitlab.dto.GitlabProject import io.ktor.http.Url -data class Project(val id: Int, val description: String, val name: String, val gitlabPath: String, val url: Url) { +data class Project(val id: Int, val description: String, val name: String, val gitlabPath: String, val url: Url, val pinned: Boolean = false) { companion object { - fun fromGitlabDto(dto: GitlabProject) = Project( - dto.id, - dto.description ?: "", - dto.name, - dto.pathWithNamespace, - Url(dto.webURL) + fun fromGitlabDto(dto: GitlabProject, isPinned: Boolean = false) = Project( + id = dto.id, + description = dto.description ?: "", + name = dto.name, + gitlabPath = dto.pathWithNamespace, + url = Url(dto.webURL), + pinned = isPinned, ) } } \ No newline at end of file diff --git a/src/main/kotlin/model/settings/Default.kt b/src/main/kotlin/model/settings/Default.kt index 3a8cf7e..74fb6a4 100644 --- a/src/main/kotlin/model/settings/Default.kt +++ b/src/main/kotlin/model/settings/Default.kt @@ -1,13 +1,17 @@ package edu.erittenhouse.gitlabtimetracker.model.settings import edu.erittenhouse.gitlabtimetracker.model.GitlabCredential -import edu.erittenhouse.gitlabtimetracker.model.settings.v1.Settings +import edu.erittenhouse.gitlabtimetracker.model.settings.v2.Settings +import edu.erittenhouse.gitlabtimetracker.model.settings.v2.SlackConfig +typealias NewestSettings = Settings +typealias NewestSlackConfig = SlackConfig /** * Constructs settings with default values using only required settings */ -fun defaultSettings(gitlabCredentials: GitlabCredential): Settings = Settings( +fun defaultSettings(gitlabCredentials: GitlabCredential): NewestSettings = NewestSettings( gitlabCredentials = gitlabCredentials, slackConfig = null, slackEnabled = false, + pinnedProjectIDs = emptySet(), ) \ No newline at end of file diff --git a/src/main/kotlin/model/settings/Migration.kt b/src/main/kotlin/model/settings/Migration.kt index 2778aa1..e1b9e8e 100644 --- a/src/main/kotlin/model/settings/Migration.kt +++ b/src/main/kotlin/model/settings/Migration.kt @@ -6,6 +6,8 @@ import edu.erittenhouse.gitlabtimetracker.model.GitlabCredential import kotlin.reflect.KClass import edu.erittenhouse.gitlabtimetracker.model.settings.v0.Settings as V0Settings import edu.erittenhouse.gitlabtimetracker.model.settings.v1.Settings as V1Settings +import edu.erittenhouse.gitlabtimetracker.model.settings.v2.Settings as V2Settings +import edu.erittenhouse.gitlabtimetracker.model.settings.v2.SlackConfig as V2SlackConfig /** * Represents a conversion from one object into another @@ -15,7 +17,7 @@ typealias Migration = (P) -> C /** * The newest migration version */ -const val newestMigrationVersion = 1 +const val newestMigrationVersion = 2 /** * Converts a settings version to the actual class for that version @@ -24,6 +26,7 @@ const val newestMigrationVersion = 1 val versionToSettings = mapOf>( 0 to V0Settings::class, 1 to V1Settings::class, + 2 to V2Settings::class, ) /** @@ -33,6 +36,7 @@ val versionToSettings = mapOf>( */ val settingsMigrations = mapOf>( 0 to ::`v0 to v1`, + 1 to ::`v1 to v2`, ) /** @@ -49,3 +53,22 @@ fun `v0 to v1`(previousVersion: VersionedSettings): V1Settings? { slackEnabled = false, ) } + +/** + * Converts from settings V1 to V2 + */ +fun `v1 to v2`(previousVersion: VersionedSettings): V2Settings? { + val convertedSettings = previousVersion as? V1Settings ?: return null + return V2Settings( + gitlabCredentials = convertedSettings.gitlabCredentials, + slackConfig = convertedSettings.slackConfig?.let { previousSlackCfg -> + V2SlackConfig( + credentialAndTeam = previousSlackCfg.credentialAndTeam, + statusEmoji = previousSlackCfg.statusEmoji, + slackStatusFormat = previousSlackCfg.slackStatusFormat, + ) + }, + slackEnabled = convertedSettings.slackEnabled, + pinnedProjectIDs = emptySet(), + ) +} diff --git a/src/main/kotlin/model/settings/v2/Settings.kt b/src/main/kotlin/model/settings/v2/Settings.kt new file mode 100644 index 0000000..c0a9d81 --- /dev/null +++ b/src/main/kotlin/model/settings/v2/Settings.kt @@ -0,0 +1,11 @@ +package edu.erittenhouse.gitlabtimetracker.model.settings.v2 + +import edu.erittenhouse.gitlabtimetracker.model.GitlabCredential +import edu.erittenhouse.gitlabtimetracker.model.settings.VersionedSettings + +data class Settings( + val gitlabCredentials: GitlabCredential, + val slackConfig: SlackConfig?, + val slackEnabled: Boolean, + val pinnedProjectIDs: Set, +) : VersionedSettings(2) \ No newline at end of file diff --git a/src/main/kotlin/model/settings/v2/SlackConfig.kt b/src/main/kotlin/model/settings/v2/SlackConfig.kt new file mode 100644 index 0000000..797dfd0 --- /dev/null +++ b/src/main/kotlin/model/settings/v2/SlackConfig.kt @@ -0,0 +1,5 @@ +package edu.erittenhouse.gitlabtimetracker.model.settings.v2 + +import edu.erittenhouse.gitlabtimetracker.model.SlackCredential + +data class SlackConfig(val credentialAndTeam: SlackCredential, val statusEmoji: String, val slackStatusFormat: String) diff --git a/src/main/kotlin/ui/fragment/ProjectListCellFragment.kt b/src/main/kotlin/ui/fragment/ProjectListCellFragment.kt index 392ee1e..974bcb0 100644 --- a/src/main/kotlin/ui/fragment/ProjectListCellFragment.kt +++ b/src/main/kotlin/ui/fragment/ProjectListCellFragment.kt @@ -1,18 +1,26 @@ package edu.erittenhouse.gitlabtimetracker.ui.fragment +import edu.erittenhouse.gitlabtimetracker.controller.ProjectController import edu.erittenhouse.gitlabtimetracker.model.Project import edu.erittenhouse.gitlabtimetracker.ui.style.Images import edu.erittenhouse.gitlabtimetracker.ui.style.LayoutStyles import edu.erittenhouse.gitlabtimetracker.ui.style.TypographyStyles import edu.erittenhouse.gitlabtimetracker.ui.util.extensions.flexspacer +import edu.erittenhouse.gitlabtimetracker.ui.util.suspension.SuspendingListCellFragment import javafx.beans.property.SimpleStringProperty +import javafx.scene.control.ToggleButton import tornadofx.* -class ProjectListCellFragment : ListCellFragment() { +class ProjectListCellFragment : SuspendingListCellFragment() { private val projectText = SimpleStringProperty("") private val projectDescription = SimpleStringProperty("") private val projectGitlabPath = SimpleStringProperty("") + private var projectID: Int = -1 private var projectUrl: String = "" + private var projectPinned = false + + private val projectController by inject() + private var pinToggle by singleAssign() init { itemProperty.onChange { updateProperties(it) } @@ -23,6 +31,9 @@ class ProjectListCellFragment : ListCellFragment() { projectText.set("Project: ${newProject.name}") setTruncatedDescription(newProject.description) projectGitlabPath.set(newProject.gitlabPath) + projectPinned = newProject.pinned + pinToggle.isSelected = newProject.pinned + projectID = newProject.id projectUrl = newProject.url.toString() } @@ -56,6 +67,15 @@ class ProjectListCellFragment : ListCellFragment() { addClass(TypographyStyles.metadata) } flexspacer() + pinToggle = togglebutton { + tooltip("Pin/unpin project") + imageview(Images.pin) + + suspendingAction { + val currentlyPinned = projectPinned + if (currentlyPinned) projectController.unpinProject(projectID) else projectController.pinProject(projectID) + } + } button { tooltip("Go to project") imageview(Images.newWindow) diff --git a/src/main/kotlin/ui/style/Images.kt b/src/main/kotlin/ui/style/Images.kt index 6b8fe51..07bdbf7 100644 --- a/src/main/kotlin/ui/style/Images.kt +++ b/src/main/kotlin/ui/style/Images.kt @@ -5,6 +5,7 @@ object Images { const val loadingPlaceholder = "/LoadingPlaceholder.jpg" const val login = "/LogIn.png" const val newWindow = "/NewWindow.png" + const val pin = "/Pin.png" const val play = "/Play.png" const val refreshIssues = "/RefreshIssues.png" const val searchIssues = "/SearchIssues.png" diff --git a/src/main/resources/Pin.png b/src/main/resources/Pin.png new file mode 100644 index 0000000000000000000000000000000000000000..7b08e146f86cf34330188520ff65f616407bac3f GIT binary patch literal 795 zcmV+$1LXXPP)EX>4Tx04R}tkv&MmKpe$iQ>7vm2Rn#5WT;M7L`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4RLxj8AiNQwVT3N2zhIPS;0dyl(!fKV?p&FUBjG~G5+ ziMW`_u8N^o1ThE!V+hO4GUg;H3E%N`j{slqVm! z;Ji;9VMSRbJ|`YE>4LCuVzla{SV+-++{ZuQ`XzEHkXw)3%``B?BCqVESxYAqxN*$Q_B)!(s zqDMgQHgIv>(v&^matG*tGGtSBr65fqp9kL0=$o=Y-!0I+=JnRx$LRx*rmm7Vz`-Ff zQlRX0pLch)_xA6ZW`93TwsM#>>OP17000JJOGiWi{{a60|De66lK=n!32;bRa{vG? zBLDy{BLR4&KXw2B00(qQO+^Rg0~-Q2D|Olj#Q*>R8FWQhbVF}#ZDnqB07G(RVRU6= zAa`kWXdp*PO;A^X4i^9b0Q5;jK~y-)z0xsCgFz4l;LniKSPN-VsDnZ}3vEQafFR@o zdI}HW5$voajTf-8u#gl6l(Er9u#O(UW&`18{RtutEG+NMzL|YI{;MKlwT7=$8f$$Q z*vCm`ek>B5B^TH&0uK?f-YSrpFhs9FBOH`_zbVqGd|`-3OmK?pMoS+zb5FXJb>Hxc z4O~|EJ4`b3>nDLvY~l%94Q`Z~TO5|hdzclxhe2jeTay?OeO%! Date: Sat, 27 Mar 2021 01:24:09 -0400 Subject: [PATCH 2/2] Changes from MR --- src/main/kotlin/controller/ProjectController.kt | 11 ++++++----- src/main/kotlin/controller/SlackController.kt | 6 +++--- src/main/kotlin/io/SettingsManager.kt | 2 +- src/main/kotlin/model/settings/v2/Settings.kt | 2 +- src/test/kotlin/controller/ProjectControllerTest.kt | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/controller/ProjectController.kt b/src/main/kotlin/controller/ProjectController.kt index b0dd78e..3ca5a0b 100644 --- a/src/main/kotlin/controller/ProjectController.kt +++ b/src/main/kotlin/controller/ProjectController.kt @@ -4,13 +4,12 @@ import edu.erittenhouse.gitlabtimetracker.controller.result.ProjectFetchResult import edu.erittenhouse.gitlabtimetracker.gitlab.GitlabAPI import edu.erittenhouse.gitlabtimetracker.io.SettingsManager import edu.erittenhouse.gitlabtimetracker.model.Project -import edu.erittenhouse.gitlabtimetracker.ui.util.suspension.SuspendingController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.javafx.JavaFx import kotlinx.coroutines.withContext import tornadofx.* -class ProjectController : SuspendingController() { +class ProjectController : Controller() { private val gitlabAPI by inject() private val credentialController by inject() private val settings = SettingsManager(find().fileLocation) @@ -25,8 +24,9 @@ class ProjectController : SuspendingController() { suspend fun fetchProjects(): ProjectFetchResult { val credentials = credentialController.credentials ?: return ProjectFetchResult.NoCredentials val pinnedProjectIDs = settings.getPinnedProjects() - val fullProjectList = gitlabAPI.project.listUserMemberProjects(credentials).map { Project.fromGitlabDto(it, isPinned = it.id in pinnedProjectIDs) } - val (localPinnedProjects, localUnpinnedProjects) = fullProjectList.partition { it.pinned } + val (localPinnedProjects, localUnpinnedProjects) = gitlabAPI.project.listUserMemberProjects(credentials).asSequence() + .map { Project.fromGitlabDto(it, isPinned = it.id in pinnedProjectIDs) } + .partition { it.pinned } pinnedProjects = localPinnedProjects unpinnedProjects = localUnpinnedProjects @@ -67,6 +67,7 @@ class ProjectController : SuspendingController() { val localUnpinnedProjects = unpinnedProjects.toMutableList() val projectToUnpinIdx = localPinnedProjects.indexOfFirst { it.id == projectID } + // If we couldn't find the specified project, just return if (projectToUnpinIdx == -1) return localUnpinnedProjects.add(0, localPinnedProjects.removeAt(projectToUnpinIdx).copy(pinned = false)) @@ -80,4 +81,4 @@ class ProjectController : SuspendingController() { projects.setAll(localPinnedProjects + localUnpinnedProjects) } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/controller/SlackController.kt b/src/main/kotlin/controller/SlackController.kt index 8e00803..ec91274 100644 --- a/src/main/kotlin/controller/SlackController.kt +++ b/src/main/kotlin/controller/SlackController.kt @@ -5,7 +5,7 @@ import edu.erittenhouse.gitlabtimetracker.controller.result.SlackLoginResult import edu.erittenhouse.gitlabtimetracker.io.SettingsManager import edu.erittenhouse.gitlabtimetracker.model.Issue import edu.erittenhouse.gitlabtimetracker.model.SlackCredential -import edu.erittenhouse.gitlabtimetracker.model.settings.v2.SlackConfig +import edu.erittenhouse.gitlabtimetracker.model.settings.NewestSlackConfig import edu.erittenhouse.gitlabtimetracker.slack.SlackAPI import edu.erittenhouse.gitlabtimetracker.slack.result.LoginResult import edu.erittenhouse.gitlabtimetracker.ui.util.suspension.SuspendingController @@ -22,7 +22,7 @@ class SlackController : SuspendingController() { private val settingsManager = SettingsManager(find().fileLocation) private val mutableEnabledState = MutableStateFlow(false) - var slackConfig: SlackConfig? = null + var slackConfig: NewestSlackConfig? = null private set val enabledState = mutableEnabledState.asStateFlow() @@ -68,7 +68,7 @@ class SlackController : SuspendingController() { * Enable the slack integration, using the provided options */ suspend fun enableSlackIntegration(slackCredential: SlackCredential, emoji: String, messageFormat: String) { - val newConfig = SlackConfig(slackCredential, emoji, messageFormat) + val newConfig = NewestSlackConfig(slackCredential, emoji, messageFormat) settingsManager.setSlackConfig(true, newConfig) slackConfig = newConfig mutableEnabledState.value = true diff --git a/src/main/kotlin/io/SettingsManager.kt b/src/main/kotlin/io/SettingsManager.kt index 08921a8..66ea97a 100644 --- a/src/main/kotlin/io/SettingsManager.kt +++ b/src/main/kotlin/io/SettingsManager.kt @@ -167,4 +167,4 @@ class SettingsManager(private val fileLocation: String = System.getProperty("use * @throws SettingsErrors.DiskIOError if data exists on the disk but couldn't be read */ suspend fun getPinnedProjects(): Set = fetchSettings(fileLocation)?.pinnedProjectIDs ?: emptySet() -} \ No newline at end of file +} diff --git a/src/main/kotlin/model/settings/v2/Settings.kt b/src/main/kotlin/model/settings/v2/Settings.kt index c0a9d81..6fc7f28 100644 --- a/src/main/kotlin/model/settings/v2/Settings.kt +++ b/src/main/kotlin/model/settings/v2/Settings.kt @@ -8,4 +8,4 @@ data class Settings( val slackConfig: SlackConfig?, val slackEnabled: Boolean, val pinnedProjectIDs: Set, -) : VersionedSettings(2) \ No newline at end of file +) : VersionedSettings(2) diff --git a/src/test/kotlin/controller/ProjectControllerTest.kt b/src/test/kotlin/controller/ProjectControllerTest.kt index 72ad64f..6aa4764 100644 --- a/src/test/kotlin/controller/ProjectControllerTest.kt +++ b/src/test/kotlin/controller/ProjectControllerTest.kt @@ -154,4 +154,4 @@ class ProjectControllerTest { assertEquals(listOf(true, false, false), controller.projects.map { it.pinned }) } } -} \ No newline at end of file +}