diff --git a/Zotero/Controllers/Database/Requests/EditItemFieldsDbRequest.swift b/Zotero/Controllers/Database/Requests/EditItemFieldsDbRequest.swift index 0267e1579..9d293079b 100644 --- a/Zotero/Controllers/Database/Requests/EditItemFieldsDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/EditItemFieldsDbRequest.swift @@ -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 @@ -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 + } +} + +struct EditItemFieldsDbRequest: EditItemFieldsBaseRequest, DbRequest { + 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) } } diff --git a/Zotero/Controllers/Database/Requests/EndItemDetailEditingDbRequest.swift b/Zotero/Controllers/Database/Requests/EndItemDetailEditingDbRequest.swift index ba8869162..f9e61a454 100644 --- a/Zotero/Controllers/Database/Requests/EndItemDetailEditingDbRequest.swift +++ b/Zotero/Controllers/Database/Requests/EndItemDetailEditingDbRequest.swift @@ -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 } diff --git a/Zotero/Scenes/Detail/ItemDetail/ItemDetailDataCreator.swift b/Zotero/Scenes/Detail/ItemDetail/ItemDetailDataCreator.swift index 49e68802b..e035b03cd 100644 --- a/Zotero/Scenes/Detail/ItemDetail/ItemDetailDataCreator.swift +++ b/Zotero/Scenes/Detail/ItemDetail/ItemDetailDataCreator.swift @@ -7,6 +7,7 @@ // import Foundation +import OrderedCollections import CocoaLumberjackSwift @@ -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( @@ -79,7 +80,6 @@ struct ItemDetailDataCreator { creators: [:], creatorIds: [], fields: fields, - fieldIds: fieldIds, abstract: (hasAbstract ? "" : nil), dateModified: date, dateAdded: date @@ -125,10 +125,9 @@ 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] = [:] @@ -185,7 +184,6 @@ struct ItemDetailDataCreator { creators: creators, creatorIds: creatorIds, fields: fields, - fieldIds: fieldIds, abstract: abstract, dateModified: item.dateModified, dateAdded: item.dateAdded @@ -209,27 +207,27 @@ struct ItemDetailDataCreator { urlDetector: UrlDetector, doiDetector: (String) -> Bool, getExistingData: ((String, String?) -> (String?, String?))? = nil - ) throws -> ([String], [String: ItemDetailState.Field], Bool) { + ) throws -> (OrderedDictionary, Bool) { guard var 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) + var fields: OrderedDictionary = [:] + 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) ?? "" @@ -241,45 +239,30 @@ struct ItemDetailDataCreator { 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)] + additionalInfo = [.formattedDate: Formatter.dateAndTime.string(from: date), .formattedEditDate: Formatter.sqlFormat.string(from: date)] } - 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, 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] { + static func allFieldKeys(for itemType: String, schemaController: SchemaController) -> OrderedSet { 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) + var fieldKeys: OrderedSet = OrderedSet(fieldSchemas.map({ $0.field })) + // Remove title and abstract keys, those 2 are used separately in Data struct. + fieldKeys.remove(FieldKeys.Item.abstract) + if let titleKey = schemaController.titleKey(for: itemType) { + fieldKeys.remove(titleKey) } return fieldKeys } - /// 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 filtered, ordered set of keys for fields that have non-empty values. + static func filteredFieldKeys(from fields: OrderedDictionary) -> OrderedSet { + return fields.filter({ !$0.value.value.isEmpty }).keys } /// Checks whether field is tappable based on its key and value. diff --git a/Zotero/Scenes/Detail/ItemDetail/Models/ItemDetailState.swift b/Zotero/Scenes/Detail/ItemDetail/Models/ItemDetailState.swift index 1c0c75aad..c7c05fde7 100644 --- a/Zotero/Scenes/Detail/ItemDetail/Models/ItemDetailState.swift +++ b/Zotero/Scenes/Detail/ItemDetail/Models/ItemDetailState.swift @@ -7,6 +7,7 @@ // import UIKit +import OrderedCollections import CocoaLumberjackSwift import RealmSwift @@ -177,8 +178,7 @@ struct ItemDetailState: ViewModelState { var localizedType: String var creators: [String: Creator] var creatorIds: [String] - var fields: [String: Field] - var fieldIds: [String] + var fields: OrderedDictionary var abstract: String? var dateModified: Date @@ -222,7 +222,6 @@ struct ItemDetailState: ViewModelState { creators: [:], creatorIds: [], fields: [:], - fieldIds: [], abstract: nil, dateModified: date, dateAdded: date, @@ -248,6 +247,7 @@ struct ItemDetailState: ViewModelState { var data: Data var snapshot: Data? var promptSnapshot: Data? + var presentedFieldIds: OrderedSet var notes: [Note] var attachments: [Attachment] var tags: [Tag] @@ -291,6 +291,7 @@ struct ItemDetailState: ViewModelState { self.userId = userId self.changes = [] self.data = .empty + self.presentedFieldIds = [] self.attachments = [] self.notes = [] self.tags = [] diff --git a/Zotero/Scenes/Detail/ItemDetail/ViewModels/ItemDetailActionHandler.swift b/Zotero/Scenes/Detail/ItemDetail/ViewModels/ItemDetailActionHandler.swift index d3138e12f..4d5590bd4 100644 --- a/Zotero/Scenes/Detail/ItemDetail/ViewModels/ItemDetailActionHandler.swift +++ b/Zotero/Scenes/Detail/ItemDetail/ViewModels/ItemDetailActionHandler.swift @@ -249,7 +249,7 @@ struct ItemDetailActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc self.itemChanged(change, in: viewModel) } - var (data, attachments, notes, tags) = try ItemDetailDataCreator.createData( + let (data, attachments, notes, tags) = try ItemDetailDataCreator.createData( from: .existing(item: item, ignoreChildren: false), schemaController: self.schemaController, dateParser: self.dateParser, @@ -260,43 +260,43 @@ struct ItemDetailActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc doiDetector: FieldKeys.Item.isDoi ) - if !canEdit { - data.fieldIds = ItemDetailDataCreator.filteredFieldKeys(from: data.fieldIds, fields: data.fields) - } - - self.saveReloaded(data: data, attachments: attachments, notes: notes, tags: tags, isEditing: canEdit, library: library, token: token, in: viewModel) + saveReloaded(data: data, attachments: attachments, notes: notes, tags: tags, isEditing: canEdit, library: library, token: token, in: viewModel) } catch let error { DDLogError("ItemDetailActionHandler: can't load data - \(error)") self.update(viewModel: viewModel) { state in state.error = .cantCreateData } } - } - private func saveReloaded( - data: ItemDetailState.Data, - attachments: [Attachment], - notes: [Note], - tags: [Tag], - isEditing: Bool, - library: Library, - token: NotificationToken, - in viewModel: ViewModel - ) { - self.update(viewModel: viewModel) { state in - state.data = data - if state.snapshot != nil || isEditing { - state.snapshot = data - state.snapshot?.fieldIds = ItemDetailDataCreator.filteredFieldKeys(from: data.fieldIds, fields: data.fields) + func saveReloaded( + data: ItemDetailState.Data, + attachments: [Attachment], + notes: [Note], + tags: [Tag], + isEditing: Bool, + library: Library, + token: NotificationToken, + in viewModel: ViewModel + ) { + update(viewModel: viewModel) { state in + state.data = data + if state.snapshot != nil || isEditing { + state.snapshot = data + } + if isEditing && !data.isAttachment { + state.presentedFieldIds = data.fields.keys + } else { + state.presentedFieldIds = ItemDetailDataCreator.filteredFieldKeys(from: data.fields) + } + state.attachments = attachments + state.notes = notes + state.tags = tags + state.library = library + state.isLoadingData = false + state.isEditing = isEditing + state.observationToken = token + state.changes.insert(.reloadedData) } - state.attachments = attachments - state.notes = notes - state.tags = tags - state.library = library - state.isLoadingData = false - state.isEditing = isEditing - state.observationToken = token - state.changes.insert(.reloadedData) } } @@ -648,16 +648,28 @@ struct ItemDetailActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc private func startEditing(in viewModel: ViewModel) { self.update(viewModel: viewModel) { state in state.snapshot = state.data - state.data.fieldIds = ItemDetailDataCreator.allFieldKeys(for: state.data.type, schemaController: self.schemaController) + if !state.data.isAttachment { + state.presentedFieldIds = ItemDetailDataCreator.allFieldKeys(for: state.data.type, schemaController: schemaController) + } state.isEditing = true state.changes.insert(.editing) } } private func endEditing(in viewModel: ViewModel) { - guard viewModel.state.snapshot != viewModel.state.data else { return } + guard viewModel.state.snapshot != viewModel.state.data else { + update(viewModel: viewModel) { state in + state.snapshot = nil + state.presentedFieldIds = ItemDetailDataCreator.filteredFieldKeys(from: state.data.fields) + state.isEditing = false + state.type = .preview(key: state.key) + state.isSaving = false + state.changes.insert(.editing) + } + return + } - self.update(viewModel: viewModel) { state in + update(viewModel: viewModel) { state in state.isSaving = true } @@ -700,15 +712,21 @@ struct ItemDetailActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc newState.data.fields[updated.key] = updated } - var requests: [DbRequest] = [EndItemDetailEditingDbRequest(libraryId: state.library.identifier, itemKey: state.key)] - if !updatedFields.isEmpty { - requests.insert(EditItemFieldsDbRequest(key: state.key, libraryId: state.library.identifier, fieldValues: updatedFields, dateParser: dateParser), at: 0) + let endEditingRequest = EndItemDetailEditingDbRequest(libraryId: state.library.identifier, itemKey: state.key) + var dateModified: Date? + try dbStorage.perform(on: queue) { coordinator in + if !updatedFields.isEmpty { + let request = EditItemFieldsDbResponseRequest(key: state.key, libraryId: state.library.identifier, fieldValues: updatedFields, dateParser: dateParser) + dateModified = try coordinator.perform(request: request) + } + try coordinator.perform(request: endEditingRequest) } - try self.dbStorage.perform(writeRequests: requests, on: queue) - newState.data.dateModified = Date() + if let dateModified { + newState.data.dateModified = dateModified + } newState.snapshot = nil - newState.data.fieldIds = ItemDetailDataCreator.filteredFieldKeys(from: newState.data.fieldIds, fields: newState.data.fields) + newState.presentedFieldIds = ItemDetailDataCreator.filteredFieldKeys(from: newState.data.fields) newState.isEditing = false newState.type = .preview(key: newState.key) newState.changes.insert(.editing) @@ -835,6 +853,7 @@ struct ItemDetailActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc self.update(viewModel: viewModel) { state in if droppedFields.isEmpty { state.data = itemData + state.presentedFieldIds = ItemDetailDataCreator.allFieldKeys(for: state.data.type, schemaController: schemaController) state.changes.insert(.type) } else { // Notify the user, that some fields with values will be dropped @@ -862,7 +881,7 @@ struct ItemDetailActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc throw ItemDetailError.typeNotSupported(type) } - let (fieldIds, fields, hasAbstract) = try ItemDetailDataCreator.fieldData( + let (fields, hasAbstract) = try ItemDetailDataCreator.fieldData( for: type, schemaController: self.schemaController, dateParser: self.dateParser, @@ -884,7 +903,6 @@ struct ItemDetailActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc data.isAttachment = type == ItemTypes.attachment data.localizedType = localizedType data.fields = fields - data.fieldIds = fieldIds data.abstract = hasAbstract ? (originalData.abstract ?? "") : nil data.creators = try creators(for: type, from: originalData.creators) data.creatorIds = originalData.creatorIds @@ -955,10 +973,19 @@ struct ItemDetailActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc } let keyPair = KeyBaseKeyPair(key: key, baseKey: (key != FieldKeys.Item.title ? FieldKeys.Item.title : nil)) - let request = EditItemFieldsDbRequest(key: viewModel.state.key, libraryId: viewModel.state.library.identifier, fieldValues: [keyPair: viewModel.state.data.title], dateParser: dateParser) - self.perform(request: request) { error in - guard let error else { return } - DDLogError("ItemDetailActionHandler: can't store title - \(error)") + let request = EditItemFieldsDbResponseRequest(key: viewModel.state.key, libraryId: viewModel.state.library.identifier, fieldValues: [keyPair: viewModel.state.data.title], dateParser: dateParser) + perform(request: request, invalidateRealm: false) { [weak viewModel] result in + switch result { + case .success(let dateModified): + guard let viewModel, let dateModified else { return } + update(viewModel: viewModel) { state in + state.data.dateModified = dateModified + state.reload = .section(.dates) + } + + case .failure(let error): + DDLogError("ItemDetailActionHandler: can't store title - \(error)") + } } } @@ -968,15 +995,24 @@ struct ItemDetailActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc state.reload = .row(.abstract) } - let request = EditItemFieldsDbRequest( + let request = EditItemFieldsDbResponseRequest( key: viewModel.state.key, libraryId: viewModel.state.library.identifier, fieldValues: [KeyBaseKeyPair(key: FieldKeys.Item.abstract, baseKey: nil): abstract], dateParser: dateParser ) - self.perform(request: request) { error in - guard let error else { return } - DDLogError("ItemDetailActionHandler: can't store abstract - \(error)") + perform(request: request, invalidateRealm: false) { [weak viewModel] result in + switch result { + case .success(let dateModified): + guard let viewModel, let dateModified else { return } + update(viewModel: viewModel) { state in + state.data.dateModified = dateModified + state.reload = .section(.dates) + } + + case .failure(let error): + DDLogError("ItemDetailActionHandler: can't store abstract - \(error)") + } } } @@ -999,15 +1035,24 @@ struct ItemDetailActionHandler: ViewModelActionHandler, BackgroundDbProcessingAc state.reload = .row(.field(key: field.key, multiline: (field.id == FieldKeys.Item.extra))) } - let request = EditItemFieldsDbRequest( + let request = EditItemFieldsDbResponseRequest( key: viewModel.state.key, libraryId: viewModel.state.library.identifier, fieldValues: [KeyBaseKeyPair(key: field.key, baseKey: field.baseField): field.value], dateParser: dateParser ) - self.perform(request: request) { error in - guard let error else { return } - DDLogError("ItemDetailActionHandler: can't store field \(error)") + perform(request: request, invalidateRealm: false) { [weak viewModel] result in + switch result { + case .success(let dateModified): + guard let viewModel, let dateModified else { return } + update(viewModel: viewModel) { state in + state.data.dateModified = dateModified + state.reload = .section(.dates) + } + + case .failure(let error): + DDLogError("ItemDetailActionHandler: can't store field - \(error)") + } } } diff --git a/Zotero/Scenes/Detail/ItemDetail/Views/ItemDetailCollectionViewHandler.swift b/Zotero/Scenes/Detail/ItemDetail/Views/ItemDetailCollectionViewHandler.swift index cc7fcdba0..2772cc633 100644 --- a/Zotero/Scenes/Detail/ItemDetail/Views/ItemDetailCollectionViewHandler.swift +++ b/Zotero/Scenes/Detail/ItemDetail/Views/ItemDetailCollectionViewHandler.swift @@ -457,7 +457,8 @@ final class ItemDetailCollectionViewHandler: NSObject { guard let self else { return } // Assign new id to all sections, just reload everything let id = UUID().uuidString - let sections = sections(for: state.data, isEditing: state.isEditing, library: state.library).map({ SectionType(identifier: id, section: $0) }) + let sections = sections(for: state.data, hasPresentedFields: !state.presentedFieldIds.isEmpty, isEditing: state.isEditing, library: state.library) + .map({ SectionType(identifier: id, section: $0) }) var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections(sections) for section in sections { @@ -473,23 +474,29 @@ final class ItemDetailCollectionViewHandler: NSObject { /// - parameter data: New data. /// - parameter isEditing: Current editing table view state. /// - returns: Array of visible sections. - func sections(for data: ItemDetailState.Data, isEditing: Bool, library: Library) -> [Section] { + func sections(for data: ItemDetailState.Data, hasPresentedFields: Bool, isEditing: Bool, library: Library) -> [Section] { + // Title and item type are always visible. + var sections: [Section] = [.title, .type] + if isEditing { // Only "metadata" sections are visible during editing. - if data.isAttachment { - return [.title, .type, .fields, .dates] - } else { - return [.title, .type, .creators, .fields, .dates, .abstract] + if !data.isAttachment { + sections.append(.creators) + } + if hasPresentedFields { + sections.append(.fields) + } + sections.append(.dates) + if !data.isAttachment { + sections.append(.abstract) } + return sections } - var sections: [Section] = [.title] - // Item type is always visible - sections.append(.type) if !data.creators.isEmpty { sections.append(.creators) } - if !data.fieldIds.isEmpty { + if hasPresentedFields { sections.append(.fields) } sections.append(.dates) @@ -667,7 +674,7 @@ final class ItemDetailCollectionViewHandler: NSObject { return [.dateAdded(state.data.dateAdded), .dateModified(state.data.dateModified)] case .fields: - return state.data.fieldIds.compactMap({ fieldId in + return state.presentedFieldIds.compactMap({ fieldId in return .field(key: fieldId, multiline: (fieldId == FieldKeys.Item.extra)) }) diff --git a/ZoteroTests/SyncActionsSpec.swift b/ZoteroTests/SyncActionsSpec.swift index 7bdaebd32..df9ba43f5 100644 --- a/ZoteroTests/SyncActionsSpec.swift +++ b/ZoteroTests/SyncActionsSpec.swift @@ -103,7 +103,6 @@ final class SyncActionsSpec: QuickSpec { creators: [:], creatorIds: [], fields: [:], - fieldIds: [], abstract: "New abstract", dateModified: Date(), dateAdded: Date() @@ -117,7 +116,6 @@ final class SyncActionsSpec: QuickSpec { creators: [:], creatorIds: [], fields: [:], - fieldIds: [], abstract: "Some note", dateModified: Date(), dateAdded: Date() @@ -368,7 +366,6 @@ final class SyncActionsSpec: QuickSpec { creators: [:], creatorIds: [], fields: [:], - fieldIds: [], abstract: "New abstract", dateModified: Date(), dateAdded: Date() @@ -382,7 +379,6 @@ final class SyncActionsSpec: QuickSpec { creators: [:], creatorIds: [], fields: [:], - fieldIds: [], abstract: "Some note", dateModified: Date(), dateAdded: Date()