Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve item detail fields #1051

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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 @@ -58,9 +58,7 @@ struct CreateItemFromDetailDbRequest: DbResponseRequest {

// Create creators

for (offset, creatorId) in self.data.creatorIds.enumerated() {
guard let creator = self.data.creators[creatorId] else { continue }

for (offset, (_, creator)) in data.creators.enumerated() {
let rCreator = RCreator()
rCreator.uuid = UUID().uuidString
rCreator.rawType = creator.type
Expand Down
49 changes: 41 additions & 8 deletions Zotero/Controllers/Database/Requests/EditItemFieldsDbRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ import Foundation

import RealmSwift

struct EditItemFieldsDbRequest: DbRequest {
let key: String
let libraryId: LibraryIdentifier
let fieldValues: [KeyBaseKeyPair: String]
let dateParser: DateParser
protocol EditItemFieldsBaseRequest {
var key: String { get }
var libraryId: LibraryIdentifier { get }
var fieldValues: [KeyBaseKeyPair: String] { get }
var dateParser: DateParser { get }

var needsWrite: Bool { return true }
func processAndReturnResponse(in database: Realm) throws -> Date?
}

func process(in database: Realm) throws {
guard !fieldValues.isEmpty, let item = database.objects(RItem.self).uniqueObject(key: key, libraryId: libraryId) else { return }
extension EditItemFieldsBaseRequest {
func processAndReturnResponse(in database: Realm) throws -> Date? {
guard !fieldValues.isEmpty, let item = database.objects(RItem.self).uniqueObject(key: key, libraryId: libraryId) else { return nil }

var didChange = false

Expand Down Expand Up @@ -60,6 +62,37 @@ struct EditItemFieldsDbRequest: DbRequest {
item.changes.append(RObjectChange.create(changes: RItemChanges.fields))
item.changeType = .user
item.dateModified = Date()
return item.dateModified
}

return nil
Copy link
Contributor

@michalrentka michalrentka Jan 10, 2025

Choose a reason for hiding this comment

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

I guess if nothing changed we could throw an error so that you don't have to check whether dateModified is not nil when processing this request. It could possibly help with debugging some time, we log the error, we don't log this being nil.

}
}

struct EditItemFieldsDbRequest: EditItemFieldsBaseRequest, DbRequest {
mvasilak marked this conversation as resolved.
Show resolved Hide resolved
let key: String
let libraryId: LibraryIdentifier
let fieldValues: [KeyBaseKeyPair: String]
let dateParser: DateParser

var needsWrite: Bool { return true }

func process(in database: Realm) throws {
_ = try processAndReturnResponse(in: database)
}
}

struct EditItemFieldsDbResponseRequest: EditItemFieldsBaseRequest, DbResponseRequest {
typealias Response = Date?

let key: String
let libraryId: LibraryIdentifier
let fieldValues: [KeyBaseKeyPair: String]
let dateParser: DateParser

var needsWrite: Bool { return true }

func process(in database: Realm) throws -> Date? {
return try processAndReturnResponse(in: database)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ struct EditItemFromDetailDbRequest: DbRequest {
private func updateCreators(with data: ItemDetailState.Data, snapshot: ItemDetailState.Data, item: RItem, changes: inout RItemChanges, database: Realm) {
guard data.creators != snapshot.creators else { return }
database.delete(item.creators)
for (offset, creatorId) in data.creatorIds.enumerated() {
guard let creator = data.creators[creatorId] else { continue }

for (offset, (_, creator)) in data.creators.enumerated() {
let rCreator = RCreator()
rCreator.uuid = UUID().uuidString
rCreator.rawType = creator.type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import OrderedCollections

import RealmSwift

Expand All @@ -15,8 +16,7 @@ struct EditTypeItemDetailDbRequest: DbRequest {
let libraryId: LibraryIdentifier
let type: String
var fields: [ItemDetailState.Field]
let creatorIds: [String]
let creators: [String: ItemDetailState.Creator]
let creators: OrderedDictionary<String, ItemDetailState.Creator>
let dateParser: DateParser

var needsWrite: Bool { return true }
Expand All @@ -28,7 +28,7 @@ struct EditTypeItemDetailDbRequest: DbRequest {

var changes: RItemChanges = [.type]
update(fields: fields, item: item, changes: &changes, database: database)
update(creatorIds: creatorIds, creators: creators, item: item, changes: &changes, database: database)
update(creators: creators, item: item, changes: &changes, database: database)
item.changes.append(RObjectChange.create(changes: changes))
}

Expand Down Expand Up @@ -92,17 +92,17 @@ struct EditTypeItemDetailDbRequest: DbRequest {
}
}

private func update(creatorIds: [String], creators: [String: ItemDetailState.Creator], item: RItem, changes: inout RItemChanges, database: Realm) {
private func update(creators: OrderedDictionary<String, ItemDetailState.Creator>, item: RItem, changes: inout RItemChanges, database: Realm) {
// Remove creator types which don't exist for this item type
let toRemove = item.creators.filter("not uuid in %@", creatorIds)
let toRemove = item.creators.filter("not uuid in %@", creators.keys)
if !toRemove.isEmpty {
changes.insert(.creators)
}
database.delete(toRemove)

for creatorId in creatorIds {
for (creatorId, creator) in creators {
// When changing item type, only thing that can change for creator is it's type
guard let creator = creators[creatorId], let rCreator = item.creators.filter("uuid == %@", creatorId).first, rCreator.rawType != creator.type else { continue }
guard let rCreator = item.creators.filter("uuid == %@", creatorId).first, rCreator.rawType != creator.type else { continue }
rCreator.rawType = creator.type
changes.insert(.creators)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ struct EndItemDetailEditingDbRequest: DbRequest {

func process(in database: Realm) throws {
guard let item = database.objects(RItem.self).uniqueObject(key: itemKey, libraryId: libraryId) else { return }
item.dateModified = Date()
item.changesSyncPaused = false
item.changeType = .user
}
Expand Down
116 changes: 53 additions & 63 deletions Zotero/Scenes/Detail/ItemDetail/ItemDetailDataCreator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import OrderedCollections

import CocoaLumberjackSwift

Expand Down Expand Up @@ -67,7 +68,7 @@ struct ItemDetailDataCreator {
throw ItemDetailError.cantCreateData
}

let (fieldIds, fields, hasAbstract) = try fieldData(for: itemType, schemaController: schemaController, dateParser: dateParser, urlDetector: urlDetector, doiDetector: doiDetector)
let (fields, hasAbstract) = try fieldData(for: itemType, schemaController: schemaController, dateParser: dateParser, urlDetector: urlDetector, doiDetector: doiDetector)
let date = Date()
let attachments: [Attachment] = child.flatMap({ [$0] }) ?? []
let data = ItemDetailState.Data(
Expand All @@ -77,9 +78,7 @@ struct ItemDetailDataCreator {
isAttachment: (itemType == ItemTypes.attachment),
localizedType: localizedType,
creators: [:],
creatorIds: [],
fields: fields,
fieldIds: fieldIds,
abstract: (hasAbstract ? "" : nil),
dateModified: date,
dateAdded: date
Expand Down Expand Up @@ -125,13 +124,11 @@ struct ItemDetailDataCreator {
}
}

let (fieldIds, fields, _) = try fieldData(for: item.rawType, schemaController: schemaController, dateParser: dateParser,
urlDetector: urlDetector, doiDetector: doiDetector, getExistingData: { key, _ in
let (fields, _) = try fieldData(for: item.rawType, schemaController: schemaController, dateParser: dateParser, urlDetector: urlDetector, doiDetector: doiDetector) { key, _ in
return (nil, values[key])
})
}

var creatorIds: [String] = []
var creators: [String: ItemDetailState.Creator] = [:]
var creators: OrderedDictionary<String, ItemDetailState.Creator> = [:]
for creator in item.creators.sorted(byKeyPath: "orderId") {
guard let localizedType = schemaController.localized(creator: creator.rawType) else { continue }

Expand All @@ -144,7 +141,6 @@ struct ItemDetailDataCreator {
primary: schemaController.creatorIsPrimary(creator.rawType, itemType: item.rawType),
localizedType: localizedType
)
creatorIds.append(creator.id)
creators[creator.id] = creator
}

Expand Down Expand Up @@ -183,9 +179,7 @@ struct ItemDetailDataCreator {
isAttachment: (item.rawType == ItemTypes.attachment),
localizedType: localizedType,
creators: creators,
creatorIds: creatorIds,
fields: fields,
fieldIds: fieldIds,
abstract: abstract,
dateModified: item.dateModified,
dateAdded: item.dateAdded
Expand All @@ -201,85 +195,81 @@ struct ItemDetailDataCreator {
/// - parameter doiDetector: DOI detector.
/// - parameter getExistingData: Closure for getting available data for given field. It passes the field key and baseField and receives existing
/// field name and value if available.
/// - returns: Tuple with 3 values: field keys of new fields, actual fields, `Bool` indicating whether this item type contains an abstract.
/// - returns: Tuple with 2 values: orderered dictionary of fields by field key, `Bool` indicating whether this item type contains an abstract.
static func fieldData(
for itemType: String,
schemaController: SchemaController,
dateParser: DateParser,
urlDetector: UrlDetector,
doiDetector: (String) -> Bool,
getExistingData: ((String, String?) -> (String?, String?))? = nil
) throws -> ([String], [String: ItemDetailState.Field], Bool) {
guard var fieldSchemas = schemaController.fields(for: itemType) else {
) throws -> (OrderedDictionary<String, ItemDetailState.Field>, Bool) {
guard let fieldSchemas = schemaController.fields(for: itemType) else {
throw ItemDetailError.typeNotSupported(itemType)
}

var fieldKeys = fieldSchemas.map({ $0.field })
let abstractIndex = fieldKeys.firstIndex(of: FieldKeys.Item.abstract)
var hasAbstract: Bool = false
let titleKey = schemaController.titleKey(for: itemType)
let isEditable = itemType != ItemTypes.attachment
var fields: OrderedDictionary<String, ItemDetailState.Field> = [:]
for schema in fieldSchemas {
let key = schema.field
// Remove title and abstract keys, those 2 are used separately in Data struct.
if key == FieldKeys.Item.abstract {
hasAbstract = true
continue
}

// Remove title and abstract keys, those 2 are used separately in Data struct
if let index = abstractIndex {
fieldKeys.remove(at: index)
fieldSchemas.remove(at: index)
}
if let key = schemaController.titleKey(for: itemType), let index = fieldKeys.firstIndex(of: key) {
fieldKeys.remove(at: index)
fieldSchemas.remove(at: index)
}
if key == titleKey {
continue
}

var fields: [String: ItemDetailState.Field] = [:]
for (offset, key) in fieldKeys.enumerated() {
let baseField = fieldSchemas[offset].baseField
let baseField = schema.baseField
let (existingName, existingValue) = (getExistingData?(key, baseField) ?? (nil, nil))

let name = existingName ?? schemaController.localized(field: key) ?? ""
let value = existingValue ?? ""
let isTappable = ItemDetailDataCreator.isTappable(key: key, value: value, urlDetector: urlDetector, doiDetector: doiDetector)
var additionalInfo: [ItemDetailState.Field.AdditionalInfoKey: String]?

if key == FieldKeys.Item.date || baseField == FieldKeys.Item.date, let order = dateParser.parse(string: value)?.orderWithSpaces {
additionalInfo = [.dateOrder: order]
}
if key == FieldKeys.Item.accessDate, let date = Formatter.iso8601.date(from: value) {
additionalInfo = [.formattedDate: Formatter.dateAndTime.string(from: date),
.formattedEditDate: Formatter.sqlFormat.string(from: date)]
switch (key, baseField) {
case (FieldKeys.Item.date, _), (_, FieldKeys.Item.date):
if let order = dateParser.parse(string: value)?.orderWithSpaces {
additionalInfo = [.dateOrder: order]
}

case (FieldKeys.Item.accessDate, _):
if let date = Formatter.iso8601.date(from: value) {
additionalInfo = [.formattedDate: Formatter.dateAndTime.string(from: date), .formattedEditDate: Formatter.sqlFormat.string(from: date)]
}

default:
break
}

fields[key] = ItemDetailState.Field(key: key,
baseField: baseField,
name: name,
value: value,
isTitle: false,
isTappable: isTappable,
additionalInfo: additionalInfo)
fields[key] = ItemDetailState.Field(
key: key,
baseField: baseField,
name: name,
value: value,
isTitle: false,
isEditable: isEditable,
isTappable: isTappable,
additionalInfo: additionalInfo
)
}

return (fieldKeys, fields, (abstractIndex != nil))
return (fields, hasAbstract)
}

/// Returns all field keys for given item type, except those that should not appear as fields in item detail.
static func allFieldKeys(for itemType: String, schemaController: SchemaController) -> [String] {
guard let fieldSchemas = schemaController.fields(for: itemType) else { return [] }
var fieldKeys = fieldSchemas.map({ $0.field })
// Remove title and abstract keys, those 2 are used separately in Data struct
if let index = fieldKeys.firstIndex(of: FieldKeys.Item.abstract) {
fieldKeys.remove(at: index)
}
if let key = schemaController.titleKey(for: itemType), let index = fieldKeys.firstIndex(of: key) {
fieldKeys.remove(at: index)
}
return fieldKeys
/// Returns ordered set of keys for fields that have non-empty values.
static func nonEmptyFieldKeys(from fields: OrderedDictionary<String, ItemDetailState.Field>) -> OrderedSet<String> {
return fields.filter({ !$0.value.value.isEmpty }).keys
}

/// Returns filtered, sorted array of keys for fields that have non-empty values.
static func filteredFieldKeys(from fieldKeys: [String], fields: [String: ItemDetailState.Field]) -> [String] {
var newFieldKeys: [String] = []
fieldKeys.forEach { key in
if !(fields[key]?.value ?? "").isEmpty {
newFieldKeys.append(key)
}
}
return newFieldKeys
/// Returns ordered set of keys for fields that are either editable or have non-empty values.
static func editableOrNonEmptyFieldKeys(from fields: OrderedDictionary<String, ItemDetailState.Field>) -> OrderedSet<String> {
return fields.filter({ $0.value.isEditable || !$0.value.value.isEmpty }).keys
}

/// Checks whether field is tappable based on its key and value.
Expand Down
Loading