Skip to content

Commit 452b094

Browse files
Merge pull request #3455 from SwiftPackageIndex/custom-collections-basic-ui
Custom collections basic UI
2 parents f60f2a8 + 077c5f5 commit 452b094

15 files changed

+563
-25
lines changed

FrontEnd/styles/package.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,17 @@
156156
align-items: center;
157157
}
158158
}
159+
160+
li.custom-collections {
161+
grid-column-start: span 2;
162+
background-image: var(--image-tags);
163+
164+
a {
165+
display: flex;
166+
gap: 5px;
167+
align-items: center;
168+
}
169+
}
159170
}
160171

161172
section.sidebar-links {

Sources/App/Commands/Reconcile.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ func processPackageDenyList(packageList: [URL], denyList: [URL]) -> [URL] {
138138
}
139139

140140

141-
func reconcileCustomCollection(client: Client, database: Database, fullPackageList: [URL], _ dto: CustomCollection.DTO) async throws {
142-
let collection = try await CustomCollection.findOrCreate(on: database, dto)
141+
func reconcileCustomCollection(client: Client, database: Database, fullPackageList: [URL], _ details: CustomCollection.Details) async throws {
142+
let collection = try await CustomCollection.findOrCreate(on: database, details)
143143

144144
// Limit incoming URLs to 50 since this is input outside of our control
145145
@Dependency(\.packageListRepository) var packageListRepository

Sources/App/Controllers/API/API+PackageController+GetRoute+Model.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ extension API.PackageController.GetRoute {
5151
var fundingLinks: [FundingLink]
5252
var swift6Readiness: Swift6Readiness?
5353
var forkedFromInfo: ForkedFromInfo?
54+
var customCollections: [CustomCollection.Details]
5455

5556
internal init(packageId: Package.Id,
5657
repositoryOwner: String,
@@ -83,7 +84,8 @@ extension API.PackageController.GetRoute {
8384
preReleaseReference: App.Reference?,
8485
fundingLinks: [FundingLink] = [],
8586
swift6Readiness: Swift6Readiness?,
86-
forkedFromInfo: ForkedFromInfo?
87+
forkedFromInfo: ForkedFromInfo?,
88+
customCollections: [CustomCollection.Details]
8789
) {
8890
self.packageId = packageId
8991
self.repositoryOwner = repositoryOwner
@@ -126,6 +128,7 @@ extension API.PackageController.GetRoute {
126128
self.fundingLinks = fundingLinks
127129
self.swift6Readiness = swift6Readiness
128130
self.forkedFromInfo = forkedFromInfo
131+
self.customCollections = customCollections
129132
}
130133

131134
init?(result: API.PackageController.PackageResult,
@@ -136,7 +139,8 @@ extension API.PackageController.GetRoute {
136139
platformBuildInfo: BuildInfo<CompatibilityMatrix.PlatformCompatibility>?,
137140
weightedKeywords: [WeightedKeyword] = [],
138141
swift6Readiness: Swift6Readiness?,
139-
forkedFromInfo: ForkedFromInfo?) {
142+
forkedFromInfo: ForkedFromInfo?,
143+
customCollections: [CustomCollection.Details]) {
140144
// we consider certain attributes as essential and return nil (raising .notFound)
141145
let repository = result.repository
142146
guard
@@ -182,7 +186,8 @@ extension API.PackageController.GetRoute {
182186
preReleaseReference: result.preReleaseVersion?.reference,
183187
fundingLinks: result.repository.fundingLinks,
184188
swift6Readiness: swift6Readiness,
185-
forkedFromInfo: forkedFromInfo
189+
forkedFromInfo: forkedFromInfo,
190+
customCollections: customCollections
186191
)
187192

188193
}

Sources/App/Controllers/API/API+PackageController+GetRoute.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ extension API.PackageController {
4747
repository: repository)
4848
async let forkedFromInfo = forkedFromInfo(on: database, fork: packageResult.repository.forkedFrom)
4949

50+
async let customCollections = customCollections(on: database, package: packageResult.package)
51+
5052
guard
5153
let model = try await Self.Model(
5254
result: packageResult,
@@ -57,7 +59,8 @@ extension API.PackageController {
5759
platformBuildInfo: buildInfo.platform,
5860
weightedKeywords: weightedKeywords,
5961
swift6Readiness: buildInfo.swift6Readiness,
60-
forkedFromInfo: forkedFromInfo
62+
forkedFromInfo: forkedFromInfo,
63+
customCollections: customCollections
6164
),
6265
let schema = API.PackageSchema(result: packageResult)
6366
else {
@@ -96,6 +99,16 @@ extension API.PackageController.GetRoute {
9699
return .fromGitHub(url: url)
97100
}
98101
}
102+
103+
static func customCollections(on database: Database, package: Package) async -> [CustomCollection.Details] {
104+
guard Current.environment() == .development else { return [] }
105+
do {
106+
try await package.$customCollections.load(on: database)
107+
return package.customCollections.map(\.details)
108+
} catch {
109+
return []
110+
}
111+
}
99112
}
100113

101114

Sources/App/Controllers/API/Types+WithExample.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ extension API.PackageController.GetRoute.Model: WithExample {
248248
releaseReference: .tag(1, 2, 3, "1.2.3"),
249249
preReleaseReference: nil,
250250
swift6Readiness: nil,
251-
forkedFromInfo: nil)
251+
forkedFromInfo: nil,
252+
customCollections: [])
252253
}
253254
}
254255

Sources/App/Core/Dependencies/PackageListRepositoryClient.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct PackageListRepositoryClient {
2222
var fetchPackageList: @Sendable (_ client: Client) async throws -> [URL]
2323
var fetchPackageDenyList: @Sendable (_ client: Client) async throws -> [URL]
2424
var fetchCustomCollection: @Sendable (_ client: Client, _ url: URL) async throws -> [URL]
25-
var fetchCustomCollections: @Sendable (_ client: Client) async throws -> [CustomCollection.DTO]
25+
var fetchCustomCollections: @Sendable (_ client: Client) async throws -> [CustomCollection.Details]
2626
}
2727

2828

@@ -62,7 +62,7 @@ extension PackageListRepositoryClient: DependencyKey {
6262
try await client
6363
.get(Constants.customCollectionsUri)
6464
.content
65-
.decode([CustomCollection.DTO].self, using: JSONDecoder())
65+
.decode([CustomCollection.Details].self, using: JSONDecoder())
6666
}
6767
)
6868
}

Sources/App/Models/CustomCollection.swift

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,39 +47,40 @@ final class CustomCollection: @unchecked Sendable, Model, Content {
4747
@Field(key: "url")
4848
var url: URL
4949

50-
// reference fields
50+
// relationships
51+
5152
@Siblings(through: CustomCollectionPackage.self, from: \.$customCollection, to: \.$package)
5253
var packages: [Package]
5354

5455
init() { }
5556

56-
init(id: Id? = nil, createdAt: Date? = nil, updatedAt: Date? = nil, _ dto: DTO) {
57+
init(id: Id? = nil, createdAt: Date? = nil, updatedAt: Date? = nil, _ details: Details) {
5758
self.id = id
5859
self.createdAt = createdAt
5960
self.updatedAt = updatedAt
60-
self.name = dto.name
61-
self.description = dto.description
62-
self.badge = dto.badge
63-
self.url = dto.url
61+
self.name = details.name
62+
self.description = details.description
63+
self.badge = details.badge
64+
self.url = details.url
6465
}
6566
}
6667

6768

6869
extension CustomCollection {
69-
struct DTO: Codable {
70+
struct Details: Codable, Equatable {
7071
var name: String
7172
var description: String?
7273
var badge: String?
7374
var url: URL
7475
}
7576

76-
static func findOrCreate(on database: Database, _ dto: DTO) async throws -> CustomCollection {
77+
static func findOrCreate(on database: Database, _ details: Details) async throws -> CustomCollection {
7778
if let collection = try await CustomCollection.query(on: database)
78-
.filter(\.$url == dto.url)
79+
.filter(\.$url == details.url)
7980
.first() {
8081
return collection
8182
} else {
82-
let collection = CustomCollection(dto)
83+
let collection = CustomCollection(details)
8384
try await collection.save(on: database)
8485
return collection
8586
}
@@ -98,6 +99,10 @@ extension CustomCollection {
9899
let removedIDs = Set(existing.keys).subtracting(Set(incoming.keys))
99100
try await $packages.detach(existing[removedIDs], on: database)
100101
}
102+
103+
var details: Details {
104+
.init(name: name, description: description, badge: badge, url: url)
105+
}
101106
}
102107

103108

Sources/App/Models/Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ final class Package: @unchecked Sendable, Model, Content {
5757

5858
// relationships
5959

60+
@Siblings(through: CustomCollectionPackage.self, from: \.$package, to: \.$customCollection)
61+
var customCollections: [CustomCollection]
62+
6063
@Children(for: \.$package)
6164
var repositories: [Repository]
6265

Sources/App/Views/PackageController/GetRoute.Model+ext.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,19 @@ extension API.PackageController.GetRoute.Model {
398398
}
399399
}
400400

401+
func customCollectionsItem() -> Node<HTML.ListContext> {
402+
guard !customCollections.isEmpty else { return .empty }
403+
return .li(
404+
.class("custom-collections"),
405+
.forEach(customCollections, { collection in
406+
.a(
407+
.href(collection.url), // FIXME: link to custom collection page
408+
.text("\(collection.name)")
409+
)
410+
})
411+
)
412+
}
413+
401414
func latestReleaseMetadata() -> Node<HTML.ListContext> {
402415
guard let dateLink = releases.stable else { return .empty }
403416
return releaseMetadata(dateLink, title: "Latest Release", cssClass: "stable")

Sources/App/Views/PackageController/PackageShow+View.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ extension PackageShow {
180180
model.productTypeListItem(.plugin),
181181
model.targetTypeListItem(.macro),
182182
model.dataRaceSafeListItem(),
183-
model.keywordsListItem()
183+
model.keywordsListItem(),
184+
model.customCollectionsItem()
184185
)
185186
)
186187
}

0 commit comments

Comments
 (0)