Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public struct DVProjectContainer: View {

// MARK: - Properties

static let projectIconSystemName = "tray"

public let name: String
public let count: Int

Expand Down Expand Up @@ -37,7 +39,7 @@ public struct DVProjectContainer: View {
extension DVProjectContainer {

private var projectIcon: some View {
Image(systemName: "tray")
Image(systemName: DVProjectContainer.projectIconSystemName)
.dvFont(.captionLG)
.foregroundStyle(Color.dv(.gray900))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright © 2026 Devault. All rights reserved

import SwiftUI

/// 프로젝트 행 인라인 리네임 컴포넌트.
/// DVProjectContainer와 동일한 leading icon + 레이아웃에서 이름 부분을 TextField로 교체한다.
public struct DVProjectRenameContainer: View {

// MARK: - Properties

@Binding public var text: String
public var onSubmit: () -> Void
public var onCancel: () -> Void

@FocusState private var isFocused: Bool
@State private var isHandled = false

// MARK: - Init

public init(
text: Binding<String>,
onSubmit: @escaping () -> Void,
onCancel: @escaping () -> Void
) {
self._text = text
self.onSubmit = onSubmit
self.onCancel = onCancel
}

// MARK: - Body

public var body: some View {
HStack(spacing: 4) {
Image(systemName: DVProjectContainer.projectIconSystemName)
.dvFont(.captionLG)
.foregroundStyle(Color.dv(.gray900))
TextField("", text: $text)
.dvFont(.bodyMD)
.textFieldStyle(.plain)
.focused($isFocused)
.onSubmit {
isHandled = true
onSubmit()
}
.onExitCommand {
isHandled = true
onCancel()
}
}
.padding(2)
.frame(minWidth: 120, alignment: .leading)
.onAppear { isFocused = true }
.onChange(of: isFocused) { _, focused in
guard !focused, !isHandled else { return }
onSubmit()
}
}
}
7 changes: 3 additions & 4 deletions Projects/DVPresentation/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
"strings" : {
"Access your secrets on all your\nApple devices, securely encrypted." : {

},
"Content" : {
"comment" : "The title of the main content section in the preview.",
"isCommentAutoGenerated" : true
},
"Create Project" : {

Expand Down Expand Up @@ -52,6 +48,9 @@
},
"목록을 불러오지 못했어요" : {

},
"불러오지 못했어요" : {

},
"시크릿이 없어요" : {

Expand Down
30 changes: 30 additions & 0 deletions Projects/DVPresentation/Sources/Dependencies/SidebarClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright © 2026 Devault. All rights reserved

import ComposableArchitecture
import Foundation

@DependencyClient
public struct SidebarClient: Sendable {
public var fetchProjects: @Sendable () async throws -> [ProjectItem]
public var createProject: @Sendable (_ name: String) async throws -> ProjectItem
public var renameProject: @Sendable (_ id: ProjectItem.ID, _ name: String) async throws -> ProjectItem
public var deleteProject: @Sendable (_ id: ProjectItem.ID) async throws -> Void
}

extension SidebarClient: TestDependencyKey {
public static let testValue = SidebarClient()

public static let previewValue = SidebarClient(
fetchProjects: { ProjectItem.previews },
createProject: { name in ProjectItem(id: UUID(), name: name) },
renameProject: { id, name in ProjectItem(id: id, name: name) },
deleteProject: { _ in }
)
}

extension DependencyValues {
public var sidebarClient: SidebarClient {
get { self[SidebarClient.self] }
set { self[SidebarClient.self] = newValue }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ public struct AddToProjectFeature {
@ObservableState
public struct State: Equatable {
public let secretID: Secret.ID
public internal(set) var projectsState: LoadingState<IdentifiedArrayOf<Project>, ProjectUseCaseError> = .idle
public internal(set) var selectedProjectID: Project.ID?
public internal(set) var projectsState: LoadingState<IdentifiedArrayOf<ProjectItem>, SidebarError> = .idle
public internal(set) var selectedProjectID: ProjectItem.ID?
@Presents var destination: Destination.State?
@Presents var alert: AlertState<Action.Alert>?

Expand All @@ -32,14 +32,14 @@ public struct AddToProjectFeature {
// MARK: - View

case task
case didSelectProject(id: Project.ID?)
case didSelectProject(id: ProjectItem.ID?)
case didTapCreateNewProject
case didTapDone
case didTapCancel

// MARK: - Internal

case projectsResponse(Result<[Project], ProjectUseCaseError>)
case projectsResponse(Result<[ProjectItem], SidebarError>)
case linkResponse(Result<Secret.ID, SecretUseCaseError>)

// MARK: - Child
Expand Down Expand Up @@ -67,6 +67,7 @@ public struct AddToProjectFeature {

// MARK: - Dependencies

@Dependency(\.sidebarClient) var sidebarClient
@Dependency(\.secretClient) var secretClient
@Dependency(\.dismiss) var dismiss

Expand All @@ -83,10 +84,12 @@ public struct AddToProjectFeature {
state.projectsState = .loading
return .run { send in
do {
let projects = try await secretClient.fetchProjects()
let projects = try await sidebarClient.fetchProjects()
await send(.projectsResponse(.success(projects)))
} catch let error as SidebarError {
await send(.projectsResponse(.failure(error)))
} catch {
await send(.projectsResponse(.failure(ProjectUseCaseError.map(error))))
await send(.projectsResponse(.failure(.fetchFailed)))
}
}

Expand All @@ -106,14 +109,14 @@ public struct AddToProjectFeature {
state.destination = .createProject(CreateProjectFeature.State())
return .none

case .destination(.presented(.createProject(.delegate(.projectCreated(let project))))):
var projects: IdentifiedArrayOf<Project> = {
case .destination(.presented(.createProject(.delegate(.projectCreated(let item))))):
var projects: IdentifiedArrayOf<ProjectItem> = {
if case let .loaded(projects) = state.projectsState { return projects }
return []
}()
projects.append(project)
projects.append(item)
state.projectsState = .loaded(projects)
state.selectedProjectID = project.id
state.selectedProjectID = item.id
Comment on lines +112 to +119

@coderabbitai coderabbitai Bot Jul 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

초기 목록 요청과 생성 결과의 순서를 보장하세요.

로딩 중 생성한 뒤 기존 fetchProjects() 응답이 늦게 도착하면, 여기서 추가한 항목을 오래된 목록으로 덮어쓸 수 있습니다. 새 프로젝트가 메뉴에서 사라지고 선택 ID만 남습니다. 생성 후 목록 요청을 직렬화·취소하거나 요청 세대로 오래된 응답을 무시하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Projects/DVPresentation/Sources/Features/AddToProject/AddToProjectFeature.swift`
around lines 112 - 119, Update the AddToProjectFeature project-loading flow
around fetchProjects and the .projectCreated delegate handler so an in-flight
initial response cannot overwrite the newly created project. Serialize or cancel
the existing fetch before applying the creation result, or track request
generations and ignore stale responses; preserve the new item in projectsState
and keep selectedProjectID set to item.id.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후 AddToProject 담당 팀원의 TODO로 남겨둘 것.
-> 수동 append가 아닌 refetch 패턴으로

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

댓글 이미지들은 다크 테마의 macOS 앱 사이드바 UI를 보여줍니다. 첫 번째는 프로젝트 목록이 보이는 상태에서 중앙에 경고 아이콘, “목록을 불러오지 못했어요”, **“Retry”**가 표시됩니다. 두 번째는 프로젝트 “가나” 행이 파란색으로 선택되고 인라인 이름 편집 필드가 열린 모습입니다. 세 번째는 API Keys/Token, OAuth, Database, SSH & Credentials, EnvSet, Etc 카테고리 타일이 표시됩니다. 네 번째는 “Create Project” 모달과 Project Name, e.g DeVault, Cancel/Create 버튼을, 다섯 번째는 “'가나' 프로젝트를 삭제할까요?” 확인 모달과 취소/삭제 버튼을 보여줍니다.

@dlguszoo, 확인했습니다. 이 건은 현재 PR 범위에서는 보류하겠습니다.

후속 작업에서는 생성 결과를 수동 append하지 않고, 생성 성공 후 프로젝트 목록을 refetch하는 흐름으로 전환하는 것이 적절합니다. 이때 최초 fetch가 늦게 완료되어 최신 목록을 덮어쓰지 않도록, 기존 요청 취소 또는 요청 세대(generation) 관리도 함께 처리해야 합니다.

원하시면 AddToProject 담당자용 GitHub 후속 이슈로 등록하겠습니다.


✏️ Learnings added
Learnt from: dlguszoo
Repo: DevaultProject/Devault-macOS PR: 62
File: Projects/DVPresentation/Sources/Features/AddToProject/AddToProjectFeature.swift:112-119
Timestamp: 2026-07-27T19:52:44.915Z
Learning: `Projects/DVPresentation/Sources/Features/AddToProject/AddToProjectFeature.swift`의 프로젝트 생성 후 목록 동기화는 수동 `ProjectItem` append가 아니라 refetch 패턴으로 개선할 예정이며, AddToProject 담당 팀원의 후속 작업 범위이다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

return .none

case .destination:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import SwiftUI

import ComposableArchitecture
import DVDesign
import DVDomain

// MARK: - AddToProjectView

Expand Down Expand Up @@ -130,7 +129,7 @@ extension AddToProjectView {
return projects[id: id]?.name
}

private var projects: IdentifiedArrayOf<Project> {
private var projects: IdentifiedArrayOf<ProjectItem> {
if case let .loaded(projects) = store.projectsState {
return projects
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import Foundation

import ComposableArchitecture
import DVDomain

// MARK: - CreateProjectFeature

Expand Down Expand Up @@ -33,7 +32,7 @@ public struct CreateProjectFeature {

// MARK: - Internal

case createResponse(Result<Project, ProjectUseCaseError>)
case createResponse(Result<ProjectItem, SidebarError>)

// MARK: - Child

Expand All @@ -44,15 +43,15 @@ public struct CreateProjectFeature {
case delegate(Delegate)

public enum Delegate: Equatable {
case projectCreated(Project)
case projectCreated(ProjectItem)
}

public enum Alert: Equatable {}
}

// MARK: - Dependencies

@Dependency(\.secretClient) var secretClient
@Dependency(\.sidebarClient) var sidebarClient
@Dependency(\.dismiss) var dismiss

// MARK: - Init
Expand All @@ -76,20 +75,33 @@ public struct CreateProjectFeature {
state.isCreating = true
return .run { send in
do {
let project = try await secretClient.createProject(trimmedName)
await send(.createResponse(.success(project)))
let item = try await sidebarClient.createProject(trimmedName)
await send(.createResponse(.success(item)))
} catch let error as SidebarError {
await send(.createResponse(.failure(error)))
} catch {
await send(.createResponse(.failure(ProjectUseCaseError.map(error))))
await send(.createResponse(.failure(.createFailed)))
}
}

case .createResponse(.success(let project)):
case .createResponse(.success(let item)):
state.isCreating = false
return .run { send in
await send(.delegate(.projectCreated(project)))
await send(.delegate(.projectCreated(item)))
await dismiss()
}

case .createResponse(.failure(.nameTaken)):
state.isCreating = false
state.alert = AlertState {
TextState("이미 사용 중인 프로젝트 이름이에요")
} actions: {
ButtonState(role: .cancel) { TextState("확인") }
} message: {
TextState("다른 이름을 입력해주세요.")
}
return .none

case .createResponse(.failure):
state.isCreating = false
state.alert = AlertState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import DVDomain
import Foundation
import SwiftUI

enum CreatableSecretType: String, CaseIterable, Hashable {
public enum CreatableSecretType: String, CaseIterable, Hashable {
case apiKeyToken
case oauth
case database
Expand Down
Loading