diff --git a/api/v1/vocabs/PKPVocabController.php b/api/v1/vocabs/PKPVocabController.php index 465a5087282..b56f1333efb 100644 --- a/api/v1/vocabs/PKPVocabController.php +++ b/api/v1/vocabs/PKPVocabController.php @@ -125,7 +125,7 @@ public function getMany(Request $illuminateRequest): JsonResponse ->withLocales([$locale]) ->when( $term, - fn ($query) => $query->withSetting($vocab, $term, ControlledVocabEntryMatch::PARTIAL) + fn ($query) => $query->withSetting('name', $term, ControlledVocabEntryMatch::PARTIAL) ) ->get(); } else { @@ -133,12 +133,17 @@ public function getMany(Request $illuminateRequest): JsonResponse Hook::call('API::vocabs::getMany', [$vocab, &$entries, $illuminateRequest, response(), $request]); } - $data = []; - foreach ($entries as $entry) { - $data[] = $entry->getLocalizedData('name', $locale); - } - - $data = array_values(array_unique($data)); + $data = collect($entries) + ->map(fn (ControlledVocabEntry $entry): array => $entry->getEntryData($locale)) + ->unique(fn (array $entryData): string => + ($entryData[ControlledVocabEntry::CONTROLLED_VOCAB_ENTRY_IDENTIFIER] ?? '') . + ($entryData[ControlledVocabEntry::CONTROLLED_VOCAB_ENTRY_SOURCE] ?? '') . + $entryData['name'] + ) + ->values() + ->toArray(); + + Hook::call('API::vocabs::external', [$vocab, $term, $locale, &$data, &$entries, $illuminateRequest, response(), $request]); return response()->json($data, Response::HTTP_OK); } diff --git a/classes/components/forms/FieldAutosuggestPreset.php b/classes/components/forms/FieldAutosuggestPreset.php index 7a03aaa14ad..de1335bf3e5 100644 --- a/classes/components/forms/FieldAutosuggestPreset.php +++ b/classes/components/forms/FieldAutosuggestPreset.php @@ -15,6 +15,8 @@ namespace PKP\components\forms; +use PKP\controlledVocab\ControlledVocabEntry; + class FieldAutosuggestPreset extends FieldBaseAutosuggest { /** @copydoc Field::$component */ @@ -75,7 +77,7 @@ protected function mapSelected($value) } return [ 'value' => $value, - 'label' => $value, + 'label' => $value['name'] ?? $value, ]; } } diff --git a/classes/components/forms/FieldControlledVocab.php b/classes/components/forms/FieldControlledVocab.php index 447c0f85bca..05495db4579 100644 --- a/classes/components/forms/FieldControlledVocab.php +++ b/classes/components/forms/FieldControlledVocab.php @@ -15,6 +15,8 @@ namespace PKP\components\forms; +use PKP\controlledVocab\ControlledVocabEntry; + class FieldControlledVocab extends FieldBaseAutosuggest { /** @copydoc Field::$component */ @@ -58,7 +60,7 @@ public function mapSelected($value) { return [ 'value' => $value, - 'label' => $value, + 'label' => $value['name'] ?? $value, ]; } } diff --git a/classes/components/forms/publication/PKPMetadataForm.php b/classes/components/forms/publication/PKPMetadataForm.php index f36082548be..c295a85ae0c 100644 --- a/classes/components/forms/publication/PKPMetadataForm.php +++ b/classes/components/forms/publication/PKPMetadataForm.php @@ -15,6 +15,8 @@ namespace PKP\components\forms\publication; +use APP\core\Application; +use APP\facades\Repo; use APP\publication\Publication; use PKP\controlledVocab\ControlledVocab; use PKP\components\forms\FieldControlledVocab; @@ -54,7 +56,7 @@ public function __construct(string $action, array $locales, Publication $publica 'isMultilingual' => true, 'apiUrl' => str_replace('__vocab__', ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $suggestionUrlBase), 'locales' => $this->locales, - 'value' => (array) $publication->getData('keywords'), + 'value' => $this->getVocabEntryData(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD), ])); } @@ -65,7 +67,7 @@ public function __construct(string $action, array $locales, Publication $publica 'isMultilingual' => true, 'apiUrl' => str_replace('__vocab__', ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, $suggestionUrlBase), 'locales' => $this->locales, - 'value' => (array) $publication->getData('subjects'), + 'value' => $this->getVocabEntryData(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT), ])); } @@ -76,7 +78,7 @@ public function __construct(string $action, array $locales, Publication $publica 'isMultilingual' => true, 'apiUrl' => str_replace('__vocab__', ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, $suggestionUrlBase), 'locales' => $this->locales, - 'value' => (array) $publication->getData('disciplines'), + 'value' => $this->getVocabEntryData(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE), ])); } @@ -87,7 +89,7 @@ public function __construct(string $action, array $locales, Publication $publica 'isMultilingual' => true, 'apiUrl' => str_replace('__vocab__', ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, $suggestionUrlBase), 'locales' => $this->locales, - 'value' => (array) $publication->getData('supportingAgencies'), + 'value' => $this->getVocabEntryData(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY), ])); } @@ -155,4 +157,18 @@ protected function enabled(string $setting): bool } return (bool) $this->context->getData($setting); } + + /** + * Get vocab entry data + */ + protected function getVocabEntryData(string $symbolic): array + { + return Repo::controlledVocab()->getBySymbolic( + $symbolic, + Application::ASSOC_TYPE_PUBLICATION, + $this->publication->getId(), + [], + Repo::controlledVocab()::AS_ENTRY_DATA + ); + } } diff --git a/classes/controlledVocab/ControlledVocabEntry.php b/classes/controlledVocab/ControlledVocabEntry.php index 052f98d8e91..d5657c0b791 100644 --- a/classes/controlledVocab/ControlledVocabEntry.php +++ b/classes/controlledVocab/ControlledVocabEntry.php @@ -27,6 +27,9 @@ class ControlledVocabEntry extends Model { use ModelWithSettings; + + public const CONTROLLED_VOCAB_ENTRY_IDENTIFIER = 'identifier'; + public const CONTROLLED_VOCAB_ENTRY_SOURCE = 'source'; /** * @copydoc \Illuminate\Database\Eloquent\Model::$table @@ -67,6 +70,8 @@ protected function casts(): array 'controlled_vocab_entry_id' => 'integer', 'controlled_vocab_id' => 'integer', 'seq' => 'float', + self::CONTROLLED_VOCAB_ENTRY_IDENTIFIER => 'string', + self::CONTROLLED_VOCAB_ENTRY_SOURCE => 'string', ]; } @@ -91,7 +96,7 @@ public function getMultilingualProps(): array */ public function getSettings(): array { - return array_merge($this->settings, ['name']); + return array_merge($this->settings, ['name', self::CONTROLLED_VOCAB_ENTRY_IDENTIFIER, self::CONTROLLED_VOCAB_ENTRY_SOURCE]); } /** @@ -190,4 +195,23 @@ public function scopeWithSetting( ) ); } + + /** + * Get entry related data + */ + public function getEntryData(string $locale = null): ?array + { + $multilingualProps = array_flip($this->getMultilingualProps()); + $attributes = Arr::mapWithKeys($this->getSettings(), function (string $prop) use ($locale, $multilingualProps): array { + $propData = $this->getAttribute($prop); + $data = isset($multilingualProps[$prop]) && $locale ? $propData[$locale] ?? null : $propData; + return $data ? [$prop => $data] : []; + }); + + if (!isset($attributes['name'])) { + return null; + } + + return $attributes; + } } diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index 04ae0758b5c..f408fc18ff7 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -20,6 +20,8 @@ class Repository { + const AS_ENTRY_DATA = true; + /** * Fetch a Controlled Vocab by symbolic info, building it if needed. */ @@ -46,7 +48,8 @@ public function getBySymbolic( string $symbolic, int $assocType, ?int $assocId, - ?array $locales = [] + ?array $locales = [], + bool $asEntryData = !Repository::AS_ENTRY_DATA ): array { $result = []; @@ -58,9 +61,9 @@ public function getBySymbolic( ) ->when(!empty($locales), fn ($query) => $query->withLocales($locales)) ->get() - ->each(function ($entry) use (&$result) { + ->each(function ($entry) use (&$result, $asEntryData) { foreach ($entry->name as $locale => $value) { - $result[$locale][] = $value; + $result[$locale][] = $asEntryData ? $entry->getEntryData($locale) : $value; } }); @@ -80,6 +83,11 @@ public function insertBySymbolic( { $controlledVocab = $this->build($symbolic, $assocType, $assocId); $controlledVocab->load('controlledVocabEntries'); + $controlledVocabEntry = new ControlledVocabEntry; + $controlledVocabEntrySettings = $controlledVocabEntry->getSettings(); + $multilingualProps = array_flip($controlledVocabEntry->getMultilingualProps()); + $idKey = ControlledVocabEntry::CONTROLLED_VOCAB_ENTRY_IDENTIFIER; + $srcKey = ControlledVocabEntry::CONTROLLED_VOCAB_ENTRY_SOURCE; if ($deleteFirst) { ControlledVocabEntry::query() @@ -93,17 +101,28 @@ public function insertBySymbolic( collect($vocabs) ->each( fn (array|string $entries, string $locale) => collect(array_values(Arr::wrap($entries))) + ->reject(fn (string|array $vocab) => is_array($vocab) && isset($vocab[$idKey]) && !isset($vocab[$srcKey])) // Remove vocabs that have id but not source + ->unique(fn (string|array $vocab): string => ($vocab[$idKey] ?? '') . ($vocab[$srcKey] ?? '') . ($vocab['name'] ?? $vocab)) ->each( - fn (string $vocab, int $index) => + fn (array|string $vocab, int $index) => ControlledVocabEntry::create([ 'controlledVocabId' => $controlledVocab->id, 'seq' => $index + 1, - 'name' => [ - $locale => $vocab - ], + ...is_array($vocab) + ? collect($vocab) + ->only($controlledVocabEntrySettings) + ->whereNotNull() + ->map(fn ($prop, string $propName) => isset($multilingualProps[$propName]) + ? [$locale => $prop] + : $prop + ) + ->toArray() + : ['name' => [$locale => $vocab]], ]) ) ); + + $this->resequence($controlledVocab->id); } /** diff --git a/schemas/publication.json b/schemas/publication.json index 2e4864a16ca..1e773235b72 100644 --- a/schemas/publication.json +++ b/schemas/publication.json @@ -142,7 +142,24 @@ "nullable" ], "items": { - "type": "string" + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "source": { + "type": "string", + "validation": [ + "nullable" + ] + }, + "identifier": { + "type": "string", + "validation": [ + "nullable" + ] + } + } } }, "doiObject": { @@ -180,7 +197,24 @@ "nullable" ], "items": { - "type": "string" + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "source": { + "type": "string", + "validation": [ + "nullable" + ] + }, + "identifier": { + "type": "string", + "validation": [ + "nullable" + ] + } + } } }, "lastModified": { @@ -256,7 +290,24 @@ "nullable" ], "items": { - "type": "string" + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "source": { + "type": "string", + "validation": [ + "nullable" + ] + }, + "identifier": { + "type": "string", + "validation": [ + "nullable" + ] + } + } } }, "submissionId": { @@ -281,7 +332,24 @@ "nullable" ], "items": { - "type": "string" + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "source": { + "type": "string", + "validation": [ + "nullable" + ] + }, + "identifier": { + "type": "string", + "validation": [ + "nullable" + ] + } + } } }, "status": {