diff --git a/CHANGELOG.md b/CHANGELOG.md index d14c5b5e..89ce254b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Release Notes for Feed Me ## Unreleased - +- Added support for importing into relational fields that have custom sources selected. ([#1504](https://github.com/craftcms/feed-me/pull/1504)) - Fixed a bug that could occur when uploading files to an Assets field from an external URL and a new filename is provided, but we can't determine the remote file's extension. ([#1506](https://github.com/craftcms/feed-me/pull/1506)) ## 6.3.0 - 2024-08-14 diff --git a/src/fields/Categories.php b/src/fields/Categories.php index 387b2fdf..dfe235f0 100644 --- a/src/fields/Categories.php +++ b/src/fields/Categories.php @@ -6,13 +6,16 @@ use Craft; use craft\base\Element as BaseElement; use craft\elements\Category as CategoryElement; +use craft\elements\conditions\ElementConditionInterface; use craft\feedme\base\Field; use craft\feedme\base\FieldInterface; use craft\feedme\helpers\DataHelper; use craft\feedme\Plugin; use craft\fields\Categories as CategoriesField; use craft\helpers\Db; +use craft\helpers\ElementHelper; use craft\helpers\Json; +use craft\services\ElementSources; /** * @@ -90,9 +93,19 @@ public function parseField(): mixed $node = Hash::get($this->fieldInfo, 'node'); $nodeKey = null; + $groupId = null; + $customSource = null; // Get source id's for connecting - [, $groupUid] = explode(':', $source); - $groupId = Db::idByUid('{{%categorygroups}}', $groupUid); + if (str_starts_with($source, 'custom:')) { + $customSource = ElementHelper::findSource(CategoryElement::class, $source, ElementSources::CONTEXT_FIELD); + // make sure $create is nullified; we don't want to create categories for custom sources + // because of ensuring all the conditions are met + // for example, if there's condition level == 2, then how do we ensure that and (more importantly) how do we choose a parent + $create = null; + } else { + [, $groupUid] = explode(':', $source); + $groupId = Db::idByUid('{{%categorygroups}}', $groupUid); + } $foundElements = []; @@ -131,13 +144,29 @@ public function parseField(): mixed } $criteria['status'] = null; - $criteria['groupId'] = $groupId; $criteria['limit'] = $limit; $criteria[$match] = $dataValue; Craft::configure($query, $criteria); - Plugin::info('Search for existing category with query `{i}`', ['i' => Json::encode($criteria)]); + if (!empty($customSource)) { + $conditionsService = Craft::$app->getConditions(); + /** @var ElementConditionInterface $sourceCondition */ + $sourceCondition = $conditionsService->createCondition($customSource['condition']); + $sourceCondition->modifyQuery($query); + } + + // we're getting the criteria from conditions now too, so they are not included in the $criteria array; + // so, we get all the query criteria, filter out any empty or boolean ones and only show the ones that look to be filled out + $showCriteria = $criteria; + $allCriteria = $query->getCriteria(); + foreach ($allCriteria as $key => $criterion) { + if (!empty($criterion) && !is_bool($criterion)) { + $showCriteria[$key] = $criterion; + } + } + + Plugin::info('Search for existing category with query `{i}`', ['i' => Json::encode($showCriteria)]); $ids = $query->ids(); diff --git a/src/fields/Entries.php b/src/fields/Entries.php index b903fa0c..c6386467 100644 --- a/src/fields/Entries.php +++ b/src/fields/Entries.php @@ -5,6 +5,7 @@ use Cake\Utility\Hash; use Craft; use craft\base\Element as BaseElement; +use craft\elements\conditions\ElementConditionInterface; use craft\elements\Entry as EntryElement; use craft\errors\ElementNotFoundException; use craft\feedme\base\Field; @@ -13,7 +14,9 @@ use craft\feedme\Plugin; use craft\fields\Entries as EntriesField; use craft\helpers\Db; +use craft\helpers\ElementHelper; use craft\helpers\Json; +use craft\services\ElementSources; use Throwable; use yii\base\Exception; @@ -94,6 +97,7 @@ public function parseField(): mixed $nodeKey = null; $sectionIds = []; + $customSources = []; if (is_array($sources)) { foreach ($sources as $source) { @@ -103,10 +107,21 @@ public function parseField(): mixed $sectionIds[] = ($section->type == 'single') ? $section->id : ''; } } else { - [, $uid] = explode(':', $source); - $sectionIds[] = Db::idByUid('{{%sections}}', $uid); + // if the source starts with "custom:", it's a custom source, and we can't treat it like a section + if (str_starts_with($source, 'custom:')) { + $customSources[] = ElementHelper::findSource(EntryElement::class, $source, ElementSources::CONTEXT_FIELD); + } else { + [, $uid] = explode(':', $source); + $sectionIds[] = Db::idByUid('{{%sections}}', $uid); + } } } + + // if there's only one source, and it's a custom source, make sure $create is nullified; + // we don't want to create entries for custom sources because of ensuring all the conditions are met + if (count($sources) == 1 && !empty($customSources)) { + $create = null; + } } elseif ($sources === '*') { $sectionIds = null; } @@ -148,13 +163,38 @@ public function parseField(): mixed } $criteria['status'] = null; - $criteria['sectionId'] = $sectionIds; $criteria['limit'] = $limit; $criteria[$match] = $dataValue; Craft::configure($query, $criteria); - Plugin::info('Search for existing entry with query `{i}`', ['i' => Json::encode($criteria)]); + // if we have any custom sources, we want to modify the query to account for those + if (!empty($customSources)) { + $conditionsService = Craft::$app->getConditions(); + foreach ($customSources as $customSource) { + /** @var ElementConditionInterface $sourceCondition */ + $sourceCondition = $conditionsService->createCondition($customSource['condition']); + $sourceCondition->modifyQuery($query); + } + } + + if (!empty($sectionIds)) { + // now that the custom sources have been accounted for, + // we can adjust the section id to include any regular, section sources (section ids) + $query->sectionId = array_merge($query->sectionId ?? [], $sectionIds); + } + + // we're getting the criteria from conditions now too, so they are not included in the $criteria array; + // so, we get all the query criteria, filter out any empty or boolean ones and only show the ones that look to be filled out + $showCriteria = $criteria; + $allCriteria = $query->getCriteria(); + foreach ($allCriteria as $key => $criterion) { + if (!empty($criterion) && !is_bool($criterion)) { + $showCriteria[$key] = $criterion; + } + } + + Plugin::info('Search for existing entry with query `{i}`', ['i' => Json::encode($showCriteria)]); $ids = $query->ids(); diff --git a/src/fields/Users.php b/src/fields/Users.php index d00d189c..86782834 100644 --- a/src/fields/Users.php +++ b/src/fields/Users.php @@ -5,6 +5,7 @@ use Cake\Utility\Hash; use Craft; use craft\base\Element as BaseElement; +use craft\elements\conditions\ElementConditionInterface; use craft\elements\db\UserQuery; use craft\elements\User as UserElement; use craft\errors\ElementNotFoundException; @@ -14,7 +15,9 @@ use craft\feedme\Plugin; use craft\fields\Users as UsersField; use craft\helpers\Db; +use craft\helpers\ElementHelper; use craft\helpers\Json; +use craft\services\ElementSources; use Throwable; use yii\base\Exception; @@ -86,12 +89,16 @@ public function parseField(): mixed // Get source id's for connecting $groupIds = []; + $customSources = []; $isAdmin = false; $status = null; if (is_array($sources)) { // go through sources that start with "group:" and get group uid for those foreach ($sources as $source) { + if (str_starts_with($source, 'custom:')) { + $customSources[] = ElementHelper::findSource(UserElement::class, $source, ElementSources::CONTEXT_MODAL); + } if (str_starts_with($source, 'group:')) { [, $uid] = explode(':', $source); $groupIds[] = Db::idByUid('{{%usergroups}}', $uid); @@ -111,6 +118,12 @@ public function parseField(): mixed if (in_array(UserElement::STATUS_INACTIVE, $sources, true)) { $status[] = UserElement::STATUS_INACTIVE; } + + // if there's only one source, and it's a custom source, make sure $create is nullified; + // we don't want to create users for custom sources because of ensuring all the conditions are met + if (count($sources) == 1 && !empty($customSources)) { + $create = null; + } } elseif ($sources === '*') { $groupIds = null; } @@ -138,13 +151,12 @@ public function parseField(): mixed $ids = []; $criteria['status'] = null; - $criteria['groupId'] = $groupIds; $criteria['limit'] = $limit; $criteria[$match] = $dataValue; // If the only source for the Users field is "admins" we don't have to bother with this query. - if (!($isAdmin && empty($groupIds))) { - $ids = $this->_findUsers($criteria); + if (!($isAdmin && empty($groupIds) && empty($customSources))) { + $ids = $this->_findUsers($criteria, $groupIds, $customSources); $foundElements = array_merge($foundElements, $ids); } @@ -153,7 +165,7 @@ public function parseField(): mixed // So if we haven't found a match with the previous query, and field sources contains "admins", // we have to look for the user among admins too. if ($isAdmin && count($ids) === 0) { - unset($criteria['groupId']); + $criteria['groupId'] = null; $criteria['admin'] = true; $ids = $this->_findUsers($criteria); @@ -241,12 +253,38 @@ private function _createElement($dataValue, $groupId): ?int * @param $criteria * @return array|int[] */ - private function _findUsers($criteria): array + private function _findUsers($criteria, $groupIds = null, $customSources = null): array { $query = UserElement::find(); Craft::configure($query, $criteria); - Plugin::info('Search for existing user with query `{i}`', ['i' => json_encode($criteria)]); + // if we have any custom sources, we want to modify the query to account for those + if (!empty($customSources)) { + $conditionsService = Craft::$app->getConditions(); + foreach ($customSources as $customSource) { + /** @var ElementConditionInterface $sourceCondition */ + $sourceCondition = $conditionsService->createCondition($customSource['condition']); + $sourceCondition->modifyQuery($query); + } + } + + if (!empty($groupIds)) { + // now that the custom sources have been accounted for, + // we can adjust the group id to include any regular, group sources (group ids) + $query->groupId = array_merge($query->groupId ?? [], $groupIds); + } + + // we're getting the criteria from conditions now too, so they are not included in the $criteria array; + // so, we get all the query criteria, filter out any empty or boolean ones and only show the ones that look to be filled out + $showCriteria = $criteria; + $allCriteria = $query->getCriteria(); + foreach ($allCriteria as $key => $criterion) { + if (!empty($criterion) && !is_bool($criterion)) { + $showCriteria[$key] = $criterion; + } + } + + Plugin::info('Search for existing user with query `{i}`', ['i' => json_encode($showCriteria)]); $ids = $query->ids(); diff --git a/src/templates/_includes/fields/categories.html b/src/templates/_includes/fields/categories.html index e1f03c6d..33bd15fc 100644 --- a/src/templates/_includes/fields/categories.html +++ b/src/templates/_includes/fields/categories.html @@ -60,12 +60,16 @@ }) }} -
- {{ feedMeMacro.checkbox({ - label: 'Create categories if they do not exist'|t('feed-me'), - name: 'options[create]', - value: 1, - checked: hash_get(feed.fieldMapping, optionsPath ~ '.create') ?: '', - }) }} -
+ {# don't allow crating new categories if the source is custom, because we can't ensure all the conditions will be met; + e.g. level, date created or updated etc #} + {% if field is defined and field is not empty and field.source starts with 'custom:' == false %} +
+ {{ feedMeMacro.checkbox({ + label: 'Create categories if they do not exist'|t('feed-me'), + name: 'options[create]', + value: 1, + checked: hash_get(feed.fieldMapping, optionsPath ~ '.create') ?: '', + }) }} +
+ {% endif %} {% endblock %} diff --git a/src/templates/_includes/fields/entries.html b/src/templates/_includes/fields/entries.html index c9ce05a8..9b83b210 100644 --- a/src/templates/_includes/fields/entries.html +++ b/src/templates/_includes/fields/entries.html @@ -62,14 +62,17 @@ }) }} -
- {{ feedMeMacro.checkbox({ - label: 'Create entries if they do not exist'|t('feed-me'), - name: 'options[create]', - value: 1, - checked: hash_get(feed.fieldMapping, optionsPath ~ '.create') ?: '', - }) }} -
+ {# don't allow crating new entries if the only selected source is custom, because we can't ensure all the conditions will be met #} + {% if field is defined and craft.feedme.fieldHasOnlyCustomSources(field) == false %} +
+ {{ feedMeMacro.checkbox({ + label: 'Create entries if they do not exist'|t('feed-me'), + name: 'options[create]', + value: 1, + checked: hash_get(feed.fieldMapping, optionsPath ~ '.create') ?: '', + }) }} +
+ {% endif %} {% if field %}
diff --git a/src/templates/_includes/fields/users.html b/src/templates/_includes/fields/users.html index 6115f6e5..cff09a80 100644 --- a/src/templates/_includes/fields/users.html +++ b/src/templates/_includes/fields/users.html @@ -61,12 +61,15 @@ }) }}
-
- {{ feedMeMacro.checkbox({ - label: 'Create users if they do not exist'|t('feed-me'), - name: 'options[create]', - value: 1, - checked: hash_get(feed.fieldMapping, optionsPath ~ '.create') ?: '', - }) }} -
+ {# don't allow crating new entries if the only selected source is custom, because we can't ensure all the conditions will be met #} + {% if field is defined and craft.feedme.fieldHasOnlyCustomSources(field) == false %} +
+ {{ feedMeMacro.checkbox({ + label: 'Create users if they do not exist'|t('feed-me'), + name: 'options[create]', + value: 1, + checked: hash_get(feed.fieldMapping, optionsPath ~ '.create') ?: '', + }) }} +
+ {% endif %} {% endblock %} diff --git a/src/web/twig/variables/FeedMeVariable.php b/src/web/twig/variables/FeedMeVariable.php index 6db1d649..62675ab1 100644 --- a/src/web/twig/variables/FeedMeVariable.php +++ b/src/web/twig/variables/FeedMeVariable.php @@ -23,6 +23,7 @@ use craft\models\Section; use craft\models\TagGroup; use DateTime; +use Illuminate\Support\Collection; use yii\di\ServiceLocator; /** @@ -356,4 +357,26 @@ public function supportedSubField($class): bool return in_array($class, $supportedSubFields, true); } + + /** + * Check if the only sources set for a relation field are custom ones. + * + * @param mixed $field + * @return bool + */ + public function fieldHasOnlyCustomSources(mixed $field = null): bool + { + if ($field === null) { + return false; + } + + if (!isset($field['sources'])) { + return false; + } + + $sources = new Collection($field['sources']); + $nativeSources = $sources->filter(fn(string $source) => !str_starts_with($source, 'custom:')); + + return $nativeSources->isEmpty(); + } }