From 7ee4693f42f3f293075cc20602707f68ad9414e9 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 17 Jun 2026 19:02:12 -0700 Subject: [PATCH 1/8] Fixed various bugs with category/tag/global settings pages --- yii2-adapter/legacy/web/twig/variables/Cp.php | 12 +++++++++--- .../templates/settings/categories/_edit.twig | 10 +++++----- .../templates/settings/categories/index.twig | 2 +- .../resources/templates/settings/globals/_edit.twig | 4 ++-- .../resources/templates/settings/tags/_edit.twig | 4 ++-- .../resources/templates/settings/tags/index.twig | 2 +- yii2-adapter/src/DeprecatedConcepts.php | 6 +++--- 7 files changed, 23 insertions(+), 17 deletions(-) diff --git a/yii2-adapter/legacy/web/twig/variables/Cp.php b/yii2-adapter/legacy/web/twig/variables/Cp.php index 6a77e665f85..9fb85e90568 100644 --- a/yii2-adapter/legacy/web/twig/variables/Cp.php +++ b/yii2-adapter/legacy/web/twig/variables/Cp.php @@ -152,17 +152,21 @@ public static function registerEvents(): void Event::listen(function(RegisterCpSettings $event) { if (\yii\base\Event::hasHandlers(self::class, self::EVENT_REGISTER_CP_SETTINGS)) { - $yiiEvent = new RegisterCpSettingsEvent(['settings' => &$event->settings]); + $yiiEvent = new RegisterCpSettingsEvent(['settings' => $event->settings]); \yii\base\Event::trigger(self::class, self::EVENT_REGISTER_CP_SETTINGS, $yiiEvent); + + $event->settings = $yiiEvent->settings; } }); Event::listen(function(RegisterReadonlyCpSettings $event) { if (\yii\base\Event::hasHandlers(self::class, self::EVENT_REGISTER_READ_ONLY_CP_SETTINGS)) { - $yiiEvent = new RegisterCpSettingsEvent(['settings' => &$event->settings]); + $yiiEvent = new RegisterCpSettingsEvent(['settings' => $event->settings]); \yii\base\Event::trigger(self::class, self::EVENT_REGISTER_READ_ONLY_CP_SETTINGS, $yiiEvent); + + $event->settings = $yiiEvent->settings; } }); @@ -178,9 +182,11 @@ public static function registerEvents(): void Event::listen(function(FormActionsResolving $event) { if (\yii\base\Event::hasHandlers(self::class, self::EVENT_REGISTER_FORM_ACTIONS)) { - $yiiEvent = new FormActionsEvent(['formActions' => &$event->formActions]); + $yiiEvent = new FormActionsEvent(['formActions' => $event->formActions]); \yii\base\Event::trigger(self::class, self::EVENT_REGISTER_FORM_ACTIONS, $yiiEvent); + + $event->formActions = $yiiEvent->formActions; } }); } diff --git a/yii2-adapter/resources/templates/settings/categories/_edit.twig b/yii2-adapter/resources/templates/settings/categories/_edit.twig index 19029d01fce..d25fdcd0ba4 100644 --- a/yii2-adapter/resources/templates/settings/categories/_edit.twig +++ b/yii2-adapter/resources/templates/settings/categories/_edit.twig @@ -14,7 +14,7 @@ {% import '_includes/forms.twig' as forms %} -{% set headlessMode = app.config.craft.general.headlessMode %} +{% set headlessMode = craft.app.config.general.headlessMode %} {% if readOnly %} {% set contentNotice = readOnlyNotice() %} @@ -35,7 +35,7 @@ id: 'name', name: 'name', value: categoryGroup.name, - errors: categoryGroup.errors.get('name'), + errors: categoryGroup.getErrors('name'), autofocus: true, required: true, disabled: readOnly, @@ -50,7 +50,7 @@ autocorrect: false, autocapitalize: false, value: categoryGroup.handle, - errors: categoryGroup.errors.get('handle'), + errors: categoryGroup.getErrors('handle'), required: true, disabled: readOnly, }) }} @@ -62,7 +62,7 @@ name: 'maxLevels', value: categoryGroup.maxLevels, size: 5, - errors: categoryGroup.errors.get('maxLevels'), + errors: categoryGroup.getErrors('maxLevels'), disabled: readOnly, }) }} @@ -82,7 +82,7 @@
{% set siteRows = [] %} - {% set siteErrors = categoryGroup.errors.get('siteSettings') %} + {% set siteErrors = categoryGroup.getErrors('siteSettings') %} {% for site in Sites.getAllSites() %} {% set siteSettings = categoryGroup.siteSettings[site.id] ?? null %} diff --git a/yii2-adapter/resources/templates/settings/categories/index.twig b/yii2-adapter/resources/templates/settings/categories/index.twig index 0cd5d360eb6..6ee55429e8a 100644 --- a/yii2-adapter/resources/templates/settings/categories/index.twig +++ b/yii2-adapter/resources/templates/settings/categories/index.twig @@ -1,6 +1,6 @@ {% extends "_layouts/cp" %} {% set title = "Category Groups"|t('yii2-adapter') %} -{% set readOnly = not app.config.craft.general.allowAdminChanges %} +{% set readOnly = not craft.app.config.general.allowAdminChanges %} {% do registerLegacyAsset('CraftCms\\Cms\\View\\LegacyAssets\\AdminTableAsset') -%} diff --git a/yii2-adapter/resources/templates/settings/globals/_edit.twig b/yii2-adapter/resources/templates/settings/globals/_edit.twig index be552716be3..c9258d404d6 100644 --- a/yii2-adapter/resources/templates/settings/globals/_edit.twig +++ b/yii2-adapter/resources/templates/settings/globals/_edit.twig @@ -35,7 +35,7 @@ id: 'name', name: 'name', value: globalSet.name, - errors: globalSet.errors.get('name'), + errors: globalSet.getErrors('name'), autofocus: true, required: true, disabled: readOnly, @@ -50,7 +50,7 @@ autocorrect: false, autocapitalize: false, value: globalSet.handle, - errors: globalSet.errors.get('handle'), + errors: globalSet.getErrors('handle'), required: true, disabled: readOnly, }) }} diff --git a/yii2-adapter/resources/templates/settings/tags/_edit.twig b/yii2-adapter/resources/templates/settings/tags/_edit.twig index d5157f95e45..f162c526d2d 100644 --- a/yii2-adapter/resources/templates/settings/tags/_edit.twig +++ b/yii2-adapter/resources/templates/settings/tags/_edit.twig @@ -33,7 +33,7 @@ id: 'name', name: 'name', value: tagGroup.name, - errors: tagGroup.errors.get('name'), + errors: tagGroup.getErrors('name'), autofocus: true, required: true, disabled: readOnly, @@ -48,7 +48,7 @@ autocorrect: false, autocapitalize: false, value: tagGroup.handle, - errors: tagGroup.errors.get('handle'), + errors: tagGroup.getErrors('handle'), required: true, disabled: readOnly, }) }} diff --git a/yii2-adapter/resources/templates/settings/tags/index.twig b/yii2-adapter/resources/templates/settings/tags/index.twig index 72b7abaea26..0c7206dcafb 100644 --- a/yii2-adapter/resources/templates/settings/tags/index.twig +++ b/yii2-adapter/resources/templates/settings/tags/index.twig @@ -1,6 +1,6 @@ {% extends "_layouts/cp" %} {% set title = "Tag Groups"|t('yii2-adapter') %} -{% set readOnly = not app.config.craft.general.allowAdminChanges %} +{% set readOnly = not craft.app.config.general.allowAdminChanges %} {% do registerLegacyAsset('CraftCms\\Cms\\View\\LegacyAssets\\AdminTableAsset') -%} diff --git a/yii2-adapter/src/DeprecatedConcepts.php b/yii2-adapter/src/DeprecatedConcepts.php index 704d3221b14..5761e645c4d 100644 --- a/yii2-adapter/src/DeprecatedConcepts.php +++ b/yii2-adapter/src/DeprecatedConcepts.php @@ -446,19 +446,19 @@ function(RegisterCpSettingsEvent $event) { $label = t('Content'); if (DeprecatedConcepts::supportsGlobalSets()) { $event->settings[$label]['globals'] = [ - 'iconMask' => '@craftcms/resources/icons/light/globe.svg', + 'iconName' => 'light/globe', 'label' => t('Globals', category: 'yii2-adapter'), ]; } if (DeprecatedConcepts::supportsCategories()) { $event->settings[$label]['categories'] = [ - 'iconMask' => '@craftcms/resources/icons/light/sitemap.svg', + 'iconName' => 'light/sitemap', 'label' => t('Categories'), ]; } if (DeprecatedConcepts::supportsTags()) { $event->settings[$label]['tags'] = [ - 'iconMask' => '@craftcms/resources/icons/light/tags.svg', + 'iconName' => 'light/tags', 'label' => t('Tags', category: 'yii2-adapter'), ]; } From 6d344aef4c4d55a91c23844906181180c8868bce Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 17 Jun 2026 19:03:06 -0700 Subject: [PATCH 2/8] Set Craft.editableCategoryGroups in JS --- src/Cp/Cp.php | 7 ++++++- src/Cp/Events/CpDataResolving.php | 12 ++++++++++++ yii2-adapter/src/DeprecatedConcepts.php | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/Cp/Events/CpDataResolving.php diff --git a/src/Cp/Cp.php b/src/Cp/Cp.php index ad24530fd43..e3af9de3f00 100644 --- a/src/Cp/Cp.php +++ b/src/Cp/Cp.php @@ -11,6 +11,7 @@ use CraftCms\Cms\Auth\Passkeys\Passkeys; use CraftCms\Cms\Cms; use CraftCms\Cms\Config\GeneralConfig; +use CraftCms\Cms\Cp\Events\CpDataResolving; use CraftCms\Cms\Edition; use CraftCms\Cms\Element\Contracts\ElementInterface; use CraftCms\Cms\Field\Fields; @@ -165,7 +166,7 @@ private static function craftData(): array ]; } - return $data + [ + $data += [ 'allowAdminChanges' => $generalConfig->allowAdminChanges, 'allowUpdates' => $generalConfig->allowUpdates, 'allowUppercaseInSlug' => $generalConfig->allowUppercaseInSlug, @@ -217,6 +218,10 @@ private static function craftData(): array 'userIsAdmin' => $currentUser->admin, 'username' => $currentUser->username, ]; + + event($event = new CpDataResolving($data)); + + return $event->data; } private static function datepickerOptions(Locale $formattingLocale, Locale $locale): array diff --git a/src/Cp/Events/CpDataResolving.php b/src/Cp/Events/CpDataResolving.php new file mode 100644 index 00000000000..137dab6a9bb --- /dev/null +++ b/src/Cp/Events/CpDataResolving.php @@ -0,0 +1,12 @@ +areMigrationsPending(); + + if ($upToDate && DeprecatedConcepts::supportsCategories()) { + $event->data['editableCategoryGroups'] = collect(Craft::$app->getCategories()->getEditableGroups()) + ->map(fn(CategoryGroup $group) => [ + 'handle' => $group->handle, + 'id' => (int)$group->id, + 'name' => Craft::t('site', $group->name), + 'uid' => $group->uid, + ]) + ->all(); + } + }); + YiiEvent::on( UrlManager::class, UrlManager::EVENT_REGISTER_CP_URL_RULES, From d9a74df49432bc2315cd49834043e8cf00c4aa4f Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 17 Jun 2026 19:03:31 -0700 Subject: [PATCH 3/8] Add auth policies for categories/tags/globals --- yii2-adapter/src/DeprecatedConcepts.php | 14 +++++ yii2-adapter/src/Policies/CategoryPolicy.php | 61 +++++++++++++++++++ yii2-adapter/src/Policies/GlobalSetPolicy.php | 32 ++++++++++ yii2-adapter/src/Policies/TagPolicy.php | 32 ++++++++++ 4 files changed, 139 insertions(+) create mode 100644 yii2-adapter/src/Policies/CategoryPolicy.php create mode 100644 yii2-adapter/src/Policies/GlobalSetPolicy.php create mode 100644 yii2-adapter/src/Policies/TagPolicy.php diff --git a/yii2-adapter/src/DeprecatedConcepts.php b/yii2-adapter/src/DeprecatedConcepts.php index fdd103e51c5..362da7ae914 100644 --- a/yii2-adapter/src/DeprecatedConcepts.php +++ b/yii2-adapter/src/DeprecatedConcepts.php @@ -70,9 +70,13 @@ use CraftCms\Cms\Support\Facades\Twig; use CraftCms\Cms\Update\Updates; use CraftCms\Cms\View\TemplateMode; +use CraftCms\Yii2Adapter\Policies\CategoryPolicy; +use CraftCms\Yii2Adapter\Policies\GlobalSetPolicy; +use CraftCms\Yii2Adapter\Policies\TagPolicy; use GraphQL\Type\Definition\Type; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Schema; use PDOException; @@ -137,6 +141,16 @@ public static function resetSupport(): void public function boot(): void { + if (DeprecatedConcepts::supportsCategories()) { + Gate::policy(Category::class, CategoryPolicy::class); + } + if (DeprecatedConcepts::supportsGlobalSets()) { + Gate::policy(GlobalSet::class, GlobalSetPolicy::class); + } + if (DeprecatedConcepts::supportsTags()) { + Gate::policy(Tag::class, TagPolicy::class); + } + Event::listen(FieldTypesResolving::class, function(FieldTypesResolving $event) { if (DeprecatedConcepts::supportsCategories()) { $event->types->add(CategoriesField::class); diff --git a/yii2-adapter/src/Policies/CategoryPolicy.php b/yii2-adapter/src/Policies/CategoryPolicy.php new file mode 100644 index 00000000000..2e711ee912e --- /dev/null +++ b/yii2-adapter/src/Policies/CategoryPolicy.php @@ -0,0 +1,61 @@ +getGroup(); + + if ($category->getIsDraft() && $category->getIsDerivative()) { + return $category->draftCreatorId === $user->getCraftUserId() + || $user->can("viewPeerCategoryDrafts:$group->uid"); + } + + return $user->can("viewCategories:$group->uid"); + } + + public function save(CraftUser $user, Category $category): bool + { + $group = $category->getGroup(); + + if ($category->getIsDraft()) { + return $category->draftCreatorId === $user->getCraftUserId() + || $user->can("savePeerCategoryDrafts:$group->uid"); + } + + return $user->can("saveCategories:$group->uid"); + } + + public function duplicate(CraftUser $user, Category $category): bool + { + $group = $category->getGroup(); + + return $user->can("saveCategories:$group->uid"); + } + + public function delete(CraftUser $user, Category $category): bool + { + $group = $category->getGroup(); + + if ($category->getIsDraft() && $category->getIsDerivative()) { + return $category->draftCreatorId === $user->getCraftUserId() + || $user->can("deletePeerCategoryDrafts:$group->uid"); + } + + return $user->can("deleteCategories:$group->uid"); + } + + public function createDrafts(CraftUser $user, Category $category): bool + { + // Everyone with view permissions can create drafts + return true; + } +} diff --git a/yii2-adapter/src/Policies/GlobalSetPolicy.php b/yii2-adapter/src/Policies/GlobalSetPolicy.php new file mode 100644 index 00000000000..de17adbe53b --- /dev/null +++ b/yii2-adapter/src/Policies/GlobalSetPolicy.php @@ -0,0 +1,32 @@ +can("editGlobalSet:$globalSet->uid"); + } + + public function save(CraftUser $user, GlobalSet $globalSet): bool + { + return true; + } + + public function duplicate(CraftUser $user, GlobalSet $globalSet): bool + { + return false; + } + + public function delete(CraftUser $user, GlobalSet $globalSet): bool + { + return false; + } +} diff --git a/yii2-adapter/src/Policies/TagPolicy.php b/yii2-adapter/src/Policies/TagPolicy.php new file mode 100644 index 00000000000..906a6e0709c --- /dev/null +++ b/yii2-adapter/src/Policies/TagPolicy.php @@ -0,0 +1,32 @@ + Date: Fri, 19 Jun 2026 11:13:01 -0700 Subject: [PATCH 4/8] Pass legacy route params as query params instead of post --- yii2-adapter/legacy/web/Application.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yii2-adapter/legacy/web/Application.php b/yii2-adapter/legacy/web/Application.php index 2808919efef..b6cbbe4b25e 100644 --- a/yii2-adapter/legacy/web/Application.php +++ b/yii2-adapter/legacy/web/Application.php @@ -304,13 +304,13 @@ private function runLaravelAction(string $route, array $params = []): ?BaseRespo return null; } - $payload = $request->merge($params)->all(); + $query = array_merge($request->query(), $params); - unset($payload['action'], $payload['p']); + unset($query['action'], $query['p']); $internalRequest = $request->duplicate( - query: [], - request: $payload, + query: $params, + request: $request->post(), server: array_merge($request->server->all(), [ 'REQUEST_URI' => $actionUri, 'HTTP_X_CRAFT_LEGACY_ACTION_BRIDGE' => '1', From c38d247579ce2460fccb2c462bc65aaa542e3e41 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Fri, 19 Jun 2026 11:36:51 -0700 Subject: [PATCH 5/8] DeprecatedTable class --- yii2-adapter/src/Database/DeprecatedTable.php | 23 +++++++++++++++++ yii2-adapter/src/DeprecatedConcepts.php | 25 ++++++++++--------- 2 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 yii2-adapter/src/Database/DeprecatedTable.php diff --git a/yii2-adapter/src/Database/DeprecatedTable.php b/yii2-adapter/src/Database/DeprecatedTable.php new file mode 100644 index 00000000000..c31bd8984da --- /dev/null +++ b/yii2-adapter/src/Database/DeprecatedTable.php @@ -0,0 +1,23 @@ +garbageCollection->runActions(array_filter([ [HardDelete::class, [ 'tables' => array_filter([ - 'categorygroups', - 'taggroups', + DeprecatedTable::CATEGORYGROUPS, + DeprecatedTable::TAGGROUPS, ], fn(string $table) => Schema::hasTable($table)), ]], - DeprecatedConcepts::supportsCategories() ? [DeletePartialElements::class, ['elementType' => Category::class, 'table' => 'categories']] : null, - DeprecatedConcepts::supportsGlobalSets() ? [DeletePartialElements::class, ['elementType' => GlobalSet::class, 'table' => 'globalsets']] : null, - DeprecatedConcepts::supportsTags() ? [DeletePartialElements::class, ['elementType' => Tag::class, 'table' => 'tags']] : null, - DeprecatedConcepts::supportsCategories() ? [DeleteOrphanedFieldLayouts::class, ['elementType' => Category::class, 'table' => 'categorygroups']] : null, - DeprecatedConcepts::supportsGlobalSets() ? [DeleteOrphanedFieldLayouts::class, ['elementType' => GlobalSet::class, 'table' => 'globalsets']] : null, - DeprecatedConcepts::supportsTags() ? [DeleteOrphanedFieldLayouts::class, ['elementType' => Tag::class, 'table' => 'taggroups']] : null, + DeprecatedConcepts::supportsCategories() ? [DeletePartialElements::class, ['elementType' => Category::class, 'table' => DeprecatedTable::CATEGORIES]] : null, + DeprecatedConcepts::supportsGlobalSets() ? [DeletePartialElements::class, ['elementType' => GlobalSet::class, 'table' => DeprecatedTable::GLOBALSETS]] : null, + DeprecatedConcepts::supportsTags() ? [DeletePartialElements::class, ['elementType' => Tag::class, 'table' => DeprecatedTable::TAGS]] : null, + DeprecatedConcepts::supportsCategories() ? [DeleteOrphanedFieldLayouts::class, ['elementType' => Category::class, 'table' => DeprecatedTable::CATEGORYGROUPS]] : null, + DeprecatedConcepts::supportsGlobalSets() ? [DeleteOrphanedFieldLayouts::class, ['elementType' => GlobalSet::class, 'table' => DeprecatedTable::GLOBALSETS]] : null, + DeprecatedConcepts::supportsTags() ? [DeleteOrphanedFieldLayouts::class, ['elementType' => Tag::class, 'table' => DeprecatedTable::TAGGROUPS]] : null, ])); }); From 8a771e681a35d9cc4bc30ab57e23f9054cf5f157 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Fri, 19 Jun 2026 15:49:51 -0700 Subject: [PATCH 6/8] Laravel-based element query classes for deprecated elements --- yii2-adapter/legacy/elements/Category.php | 4 +- yii2-adapter/legacy/elements/GlobalSet.php | 4 +- yii2-adapter/legacy/elements/Tag.php | 4 +- .../src/Element/Queries/CategoryQuery.php | 228 ++++++++++++++++++ .../Category/QueriesCategoryGroups.php | 195 +++++++++++++++ .../Queries/Concerns/Category/QueriesRef.php | 77 ++++++ .../Queries/Concerns/Tag/QueriesTagGroups.php | 173 +++++++++++++ .../src/Element/Queries/GlobalSetQuery.php | 174 +++++++++++++ yii2-adapter/src/Element/Queries/TagQuery.php | 73 ++++++ 9 files changed, 926 insertions(+), 6 deletions(-) create mode 100644 yii2-adapter/src/Element/Queries/CategoryQuery.php create mode 100644 yii2-adapter/src/Element/Queries/Concerns/Category/QueriesCategoryGroups.php create mode 100644 yii2-adapter/src/Element/Queries/Concerns/Category/QueriesRef.php create mode 100644 yii2-adapter/src/Element/Queries/Concerns/Tag/QueriesTagGroups.php create mode 100644 yii2-adapter/src/Element/Queries/GlobalSetQuery.php create mode 100644 yii2-adapter/src/Element/Queries/TagQuery.php diff --git a/yii2-adapter/legacy/elements/Category.php b/yii2-adapter/legacy/elements/Category.php index 0afbf9e57f8..c67a0f758dc 100644 --- a/yii2-adapter/legacy/elements/Category.php +++ b/yii2-adapter/legacy/elements/Category.php @@ -16,7 +16,6 @@ use craft\elements\actions\NewChild; use craft\elements\actions\Restore; use craft\elements\conditions\categories\CategoryCondition; -use craft\elements\db\CategoryQuery; use craft\gql\interfaces\elements\Category as CategoryInterface; use craft\models\CategoryGroup; use craft\records\Category as CategoryRecord; @@ -39,6 +38,7 @@ use CraftCms\Cms\Twig\Attributes\AllowedInSandbox; use CraftCms\Cms\User\Elements\User; use CraftCms\RulesetValidation\Attributes\Ruleset; +use CraftCms\Yii2Adapter\Element\Queries\CategoryQuery; use CraftCms\Yii2Adapter\Validation\LegacyElementRules; use GraphQL\Type\Definition\Type; use Illuminate\Support\Collection; @@ -155,7 +155,7 @@ public static function hasStatuses(): bool */ public static function find(): CategoryQuery { - return new CategoryQuery(static::class); + return new CategoryQuery(); } /** diff --git a/yii2-adapter/legacy/elements/GlobalSet.php b/yii2-adapter/legacy/elements/GlobalSet.php index f33d7c232ed..bbbd736c06a 100644 --- a/yii2-adapter/legacy/elements/GlobalSet.php +++ b/yii2-adapter/legacy/elements/GlobalSet.php @@ -8,7 +8,6 @@ namespace craft\elements; use craft\behaviors\FieldLayoutBehavior; -use craft\elements\db\GlobalSetQuery; use craft\records\GlobalSet as GlobalSetRecord; use craft\validators\HandleValidator; use craft\validators\UniqueValidator; @@ -21,6 +20,7 @@ use CraftCms\Cms\Twig\Attributes\AllowedInSandbox; use CraftCms\Cms\User\Elements\User; use CraftCms\RulesetValidation\Attributes\Ruleset; +use CraftCms\Yii2Adapter\Element\Queries\GlobalSetQuery; use CraftCms\Yii2Adapter\Validation\LegacyElementRules; use Illuminate\Support\Facades\Log; use yii\base\InvalidConfigException; @@ -145,7 +145,7 @@ public function canDelete(User $user): bool */ public static function find(): GlobalSetQuery { - return new GlobalSetQuery(static::class); + return new GlobalSetQuery(); } /** diff --git a/yii2-adapter/legacy/elements/Tag.php b/yii2-adapter/legacy/elements/Tag.php index 04c6738eb24..9720eeadeb4 100644 --- a/yii2-adapter/legacy/elements/Tag.php +++ b/yii2-adapter/legacy/elements/Tag.php @@ -9,7 +9,6 @@ use Craft; use craft\elements\conditions\tags\TagCondition; -use craft\elements\db\TagQuery; use craft\gql\interfaces\elements\Tag as TagInterface; use craft\helpers\Db; use craft\models\TagGroup; @@ -21,6 +20,7 @@ use CraftCms\Cms\Twig\Attributes\AllowedInSandbox; use CraftCms\Cms\User\Elements\User; use CraftCms\RulesetValidation\Attributes\Ruleset; +use CraftCms\Yii2Adapter\Element\Queries\TagQuery; use CraftCms\Yii2Adapter\Validation\LegacyElementRules; use GraphQL\Type\Definition\Type; use yii\base\InvalidConfigException; @@ -108,7 +108,7 @@ public static function isLocalized(): bool */ public static function find(): TagQuery { - return new TagQuery(static::class); + return new TagQuery(); } /** diff --git a/yii2-adapter/src/Element/Queries/CategoryQuery.php b/yii2-adapter/src/Element/Queries/CategoryQuery.php new file mode 100644 index 00000000000..1f4f1a7d706 --- /dev/null +++ b/yii2-adapter/src/Element/Queries/CategoryQuery.php @@ -0,0 +1,228 @@ + + */ +class CategoryQuery extends ElementQuery +{ + use QueriesRef; + use QueriesCategoryGroups; + + #[Override] + public bool $withStructure { + get { + if (!isset($this->withStructure)) { + $this->withStructure = true; + } + + return $this->withStructure; + } + } + + #[Override] + protected string $table = DeprecatedTable::CATEGORIES; + + /** + * @var bool|null Whether to only return categories that the user has permission to view. + * + * @used-by editable() + */ + public ?bool $editable = null; + + public function __construct(array $config = []) + { + // Default status + if (!isset($config['status'])) { + $config['status'] = [ + Element::STATUS_ENABLED, + ]; + } + + parent::__construct(Category::class, $config); + + $this->query->addSelect([ + 'categories.groupId as groupId', + ]); + + if (Cms::config()->staticStatuses) { + $this->query->addSelect(['categories.status as status']); + } + + $this->beforeQuery(function(self $query) { + $this->applyAuthParam($query, $query->editable, 'viewCategories'); + }); + } + + /** + * Sets the [[$editable]] property. + * + * @param bool|null $value The property value (defaults to true) + * + * @uses $editable + */ + public function editable(?bool $value = true): self + { + $this->editable = $value; + + return $this; + } + + /** + * Sets the [[$savable]] property. + * + * @param bool|null $value The property value (defaults to true) + * @return self self reference + * + * @uses $savable + */ + public function savable(?bool $value = true): self + { + $this->savable = $value; + + return $this; + } + + /** + * Narrows the query results based on the categories’ statuses. + * + * Possible values include: + * + * | Value | Fetches categories… + * | - | - + * | `'live'` _(default)_ | that are live. + * | `'pending'` | that are pending (enabled with a Post Date in the future). + * | `'expired'` | that are expired (enabled with an Expiry Date in the past). + * | `'disabled'` | that are disabled. + * | `['live', 'pending']` | that are live or pending. + * | `['not', 'live', 'pending']` | that are not live or pending. + * + * --- + * + * ```twig + * {# Fetch disabled categories #} + * {% set {elements-var} = {twig-method} + * .status('disabled') + * .all() %} + * ``` + * + * ```php + * // Fetch disabled categories + * ${elements-var} = {element-class}::find() + * ->status('disabled') + * ->all(); + * ``` + */ + #[Override] + public function status(array|string|null $value): static + { + /** @var static */ + return parent::status($value); + } + + /** + * @throws QueryAbortedException + */ + private function applyAuthParam( + self $query, + ?bool $value, + string $permissionPrefix, + ): void { + if ($value === null) { + return; + } + + $user = currentUser(); + + if (!$user) { + throw new QueryAbortedException(); + } + + $groups = Craft::$app->getCategories()->getAllGroups(); + + if (empty($groups)) { + return; + } + + $query->where(function(Builder $query) use ($value, $permissionPrefix, $user, $groups) { + foreach ($groups as $group) { + if ($user->can("$permissionPrefix:$group->uid")) { + $fullyAuthorizedGroupIds[] = $group->id; + } + } + + if (!empty($fullyAuthorizedGroupIds)) { + if (count($fullyAuthorizedGroupIds) === count($groups)) { + // They have access to everything + if (!$value) { + throw new QueryAbortedException(); + } + + return; + } + + $query->orWhereIn('categories.groupId', $fullyAuthorizedGroupIds); + } + + // They don't have access to anything + if ($value) { + throw new QueryAbortedException(); + } + }, boolean: $value ? 'and' : 'and not'); + } + + #[Override] + protected function cacheTags(): array + { + $tags = []; + + if ($this->groupId) { + foreach (Arr::wrap($this->groupId) as $groupId) { + $tags[] = "group:$groupId"; + } + } + + return $tags; + } + + #[Override] + protected function fieldLayouts(): Collection + { + $this->normalizeGroupId($this); + + if ($this->groupId) { + $fieldLayouts = []; + + foreach ($this->groupId as $groupId) { + if ($group = Craft::$app->getCategories()->getGroupById($groupId)) { + $fieldLayouts[] = $group->getFieldLayout(); + } + } + + return collect($fieldLayouts); + } + + return collect(); + } +} diff --git a/yii2-adapter/src/Element/Queries/Concerns/Category/QueriesCategoryGroups.php b/yii2-adapter/src/Element/Queries/Concerns/Category/QueriesCategoryGroups.php new file mode 100644 index 00000000000..8ad9eaacc86 --- /dev/null +++ b/yii2-adapter/src/Element/Queries/Concerns/Category/QueriesCategoryGroups.php @@ -0,0 +1,195 @@ +group('news') + * ->all(); + * ``` + * ```twig + * {# fetch categories in the News group #} + * {% set categories = craft.categories() + * .group('news') + * .all() %} + * ``` + * + * @used-by group() + * @used-by groupId() + */ + public mixed $groupId = null; + + protected function initQueriesGroups(): void + { + $this->beforeQuery(function(CategoryQuery $categoryQuery) { + $this->normalizeGroupId($categoryQuery); + + if ($categoryQuery->groupId === []) { + throw new QueryAbortedException(); + } + + $this->applyGroupIdParam($categoryQuery); + }); + } + + /** + * Narrows the query results based on the groups the categories belong to. + * + * Possible values include: + * + * | Value | Fetches categories… + * | - | - + * | `'foo'` | in a group with a handle of `foo`. + * | `'not foo'` | not in a group with a handle of `foo`. + * | `['foo', 'bar']` | in a group with a handle of `foo` or `bar`. + * | `['not', 'foo', 'bar']` | not in a group with a handle of `foo` or `bar`. + * | a [[CategoryGroup|CategoryGroup]] object | in a group represented by the object. + * | `'*'` | in any group. + * + * --- + * + * ```twig + * {# Fetch categories in the Foo group #} + * {% set {elements-var} = {twig-method} + * .group('foo') + * .all() %} + * ``` + * + * ```php + * // Fetch categories in the Foo group + * ${elements-var} = {php-method} + * ->group('foo') + * ->all(); + * ``` + * + * + * @uses $groupId + */ + public function group(mixed $value): static + { + // If the value is a group handle, swap it with the group + if (is_string($value) && ($group = Craft::$app->getCategories()->getGroupByHandle($value))) { + $value = $group; + } + + if ($value instanceof CategoryGroup) { + $this->groupId = [$value->id]; + $this->structureId = $value->structureId; + } elseif ($value === '*') { + $this->groupId = Craft::$app->getCategories()->getAllGroupIds(); + } elseif (Query::normalizeParam($value, function($item) { + if (is_string($item)) { + $item = Craft::$app->getCategories()->getGroupByHandle($item); + } + + return $item instanceof CategoryGroup ? $item->id : null; + })) { + $this->groupId = $value; + } else { + $this->groupId = DB::table(DeprecatedTable::CATEGORYGROUPS) + ->whereParam('handle', $value) + ->pluck('id') + ->all(); + } + + return $this; + } + + /** + * Narrows the query results based on the groups the categories belong to, per the groups’ IDs. + * + * Possible values include: + * + * | Value | Fetches categories… + * | - | - + * | `1` | in a group with an ID of 1. + * | `'not 1'` | not in a group with an ID of 1. + * | `[1, 2]` | in a group with an ID of 1 or 2. + * | `['not', 1, 2]` | not in a group with an ID of 1 or 2. + * + * --- + * + * ```twig + * {# Fetch categories in the group with an ID of 1 #} + * {% set {elements-var} = {twig-method} + * .groupId(1) + * .all() %} + * ``` + * + * ```php + * // Fetch categories in the group with an ID of 1 + * ${elements-var} = {php-method} + * ->groupId(1) + * ->all(); + * ``` + * + * + * @uses $groupId + */ + public function groupId(mixed $value): static + { + $this->groupId = $value; + + return $this; + } + + /** + * Applies the 'groupId' param to the query being prepared. + */ + private function applyGroupIdParam(CategoryQuery $categoryQuery): void + { + if (!$categoryQuery->groupId) { + return; + } + + $categoryQuery->whereIn('categories.groupId', $categoryQuery->groupId); + + // Should we set the structureId param? + if ( + $categoryQuery->withStructure !== false && + !isset($categoryQuery->structureId) && + count($categoryQuery->groupId) === 1 + ) { + $group = Craft::$app->getCategories()->getGroupById(reset($categoryQuery->groupId)); + if ($group) { + $categoryQuery->structureId = $group->structureId; + } + } + } + + /** + * Normalizes the groupId param to an array of IDs or null + */ + private function normalizeGroupId(CategoryQuery $categoryQuery): void + { + $categoryQuery->groupId = match (true) { + empty($categoryQuery->groupId) => is_array($categoryQuery->groupId) ? [] : null, + is_numeric($categoryQuery->groupId) => [$categoryQuery->groupId], + !is_array($categoryQuery->groupId) || !Arr::isNumeric($categoryQuery->groupId) => DB::table(DeprecatedTable::CATEGORYGROUPS) + ->whereNumericParam('id', $categoryQuery->groupId) + ->pluck('id') + ->all(), + default => $categoryQuery->groupId, + }; + } +} diff --git a/yii2-adapter/src/Element/Queries/Concerns/Category/QueriesRef.php b/yii2-adapter/src/Element/Queries/Concerns/Category/QueriesRef.php new file mode 100644 index 00000000000..52b48eca603 --- /dev/null +++ b/yii2-adapter/src/Element/Queries/Concerns/Category/QueriesRef.php @@ -0,0 +1,77 @@ +beforeQuery(function(CategoryQuery $query) { + if (is_null($query->ref)) { + return; + } + + $refs = $query->ref; + if (!is_array($refs)) { + $refs = is_string($refs) ? str($refs)->explode(',') : [$refs]; + } + + $joinGroups = false; + $query->where(function(Builder $query) use (&$joinGroups, $refs) { + foreach ($refs as $ref) { + $parts = array_filter(explode('/', (string) $ref), static fn(string $part) => $part !== ''); + + if (empty($parts)) { + continue; + } + + if (count($parts) === 1) { + $query->orWhereParam('elements_sites.slug', $parts[0]); + + continue; + } + + $query->where(function(Builder $query) use ($parts) { + $query->whereParam('categorygroups.handle', $parts[0]) + ->whereParam('elements_sites.slug', $parts[1]); + }); + + $joinGroups = true; + } + }); + + if ($joinGroups) { + $this->join(new Alias(DeprecatedTable::CATEGORYGROUPS, 'categorygroups'), 'categorygroups.id', '=', 'categories.groupId'); + } + }); + } + + /** + * Narrows the query results based on a reference string. + */ + public function ref(mixed $value): static + { + $this->ref = $value; + + return $this; + } +} diff --git a/yii2-adapter/src/Element/Queries/Concerns/Tag/QueriesTagGroups.php b/yii2-adapter/src/Element/Queries/Concerns/Tag/QueriesTagGroups.php new file mode 100644 index 00000000000..ded35e61afa --- /dev/null +++ b/yii2-adapter/src/Element/Queries/Concerns/Tag/QueriesTagGroups.php @@ -0,0 +1,173 @@ +group('topics') + * ->all(); + * ``` + * ```twig + * {# fetch tags in the Topics group #} + * {% set tags = craft.tags() + * .group('topics') + * .all() %} + * ``` + * + * @used-by group() + * @used-by groupId() + */ + public mixed $groupId = null; + + protected function initQueriesTagGroups(): void + { + $this->beforeQuery(function(TagQuery $tagQuery) { + $this->normalizeGroupId($tagQuery); + $this->applyGroupIdParam($tagQuery); + }); + } + + /** + * Narrows the query results based on the groups the tags belong to. + * + * Possible values include: + * + * | Value | Fetches tags… + * | - | - + * | `'foo'` | in a group with a handle of `foo`. + * | `'not foo'` | not in a group with a handle of `foo`. + * | `['foo', 'bar']` | in a group with a handle of `foo` or `bar`. + * | `['not', 'foo', 'bar']` | not in a group with a handle of `foo` or `bar`. + * | a [[TagGroup|TagGroup]] object | in a group represented by the object. + * + * --- + * + * ```twig + * {# Fetch tags in the Foo group #} + * {% set {elements-var} = {twig-method} + * .group('foo') + * .all() %} + * ``` + * + * ```php + * // Fetch tags in the Foo group + * ${elements-var} = {php-method} + * ->group('foo') + * ->all(); + * ``` + * + * + * @uses $groupId + */ + public function group(mixed $value): static + { + if (Query::normalizeParam($value, function($item) { + if (is_string($item)) { + $item = Craft::$app->getTags()->getTagGroupByHandle($item); + } + + return $item instanceof TagGroup ? $item->id : null; + })) { + $this->groupId = $value; + } else { + $this->groupId = DB::table(DeprecatedTable::TAGGROUPS) + ->whereParam('handle', $value) + ->pluck('id') + ->all() ?: false; + } + + return $this; + } + + /** + * Narrows the query results based on the groups the tags belong to, per the groups’ IDs. + * + * Possible values include: + * + * | Value | Fetches tags… + * | - | - + * | `1` | in a group with an ID of 1. + * | `'not 1'` | not in a group with an ID of 1. + * | `[1, 2]` | in a group with an ID of 1 or 2. + * | `['not', 1, 2]` | not in a group with an ID of 1 or 2. + * + * --- + * + * ```twig + * {# Fetch tags in the group with an ID of 1 #} + * {% set {elements-var} = {twig-method} + * .groupId(1) + * .all() %} + * ``` + * + * ```php + * // Fetch tags in the group with an ID of 1 + * ${elements-var} = {php-method} + * ->groupId(1) + * ->all(); + * ``` + * + * + * @uses $groupId + */ + public function groupId(mixed $value): static + { + $this->groupId = $value; + + return $this; + } + + /** + * Applies the 'groupId' param to the query being prepared. + */ + private function applyGroupIdParam(TagQuery $tagQuery): void + { + if (!$tagQuery->groupId) { + return; + } + + $tagQuery->whereIn('tags.groupId', $tagQuery->groupId); + } + + /** + * Normalizes the groupId param to an array of IDs or null + * + * @throws QueryAbortedException + */ + private function normalizeGroupId(TagQuery $tagQuery): void + { + if ($tagQuery->groupId === false) { + throw new QueryAbortedException(); + } + + $tagQuery->groupId = match (true) { + empty($tagQuery->groupId) => null, + is_numeric($tagQuery->groupId) => [$tagQuery->groupId], + !is_array($tagQuery->groupId) || !Arr::isNumeric($tagQuery->groupId) => DB::table(DeprecatedTable::TAGGROUPS) + ->whereNumericParam('id', $tagQuery->groupId) + ->pluck('id') + ->all(), + default => $tagQuery->groupId, + }; + } +} diff --git a/yii2-adapter/src/Element/Queries/GlobalSetQuery.php b/yii2-adapter/src/Element/Queries/GlobalSetQuery.php new file mode 100644 index 00000000000..0bb3e2a3f99 --- /dev/null +++ b/yii2-adapter/src/Element/Queries/GlobalSetQuery.php @@ -0,0 +1,174 @@ + + */ +class GlobalSetQuery extends ElementQuery +{ + #[Override] + protected string $table = DeprecatedTable::GLOBALSETS; + + #[Override] + protected array $defaultOrderBy = ['globalsets.sortOrder' => SORT_ASC]; + + /** + * @var bool|null Whether to only return global sets that the user has permission to edit. + * + * @used-by editable() + */ + public ?bool $editable = null; + + /** + * @var string|string[]|null The handle(s) that the resulting global sets must have. + * + * @used-by handle() + */ + public string|array|null $handle = null; + + /** + * @var mixed The reference code(s) used to identify the element(s). + * + * This property is set when accessing elements via their reference tags, e.g. `{globalset:handle}`. + * + * @used-by ref() + */ + public mixed $ref = null; + + public function __construct(array $config = []) + { + parent::__construct(GlobalSet::class, $config); + + $this->query->addSelect([ + 'globalsets.name', + 'globalsets.handle', + 'globalsets.sortOrder', + ]); + + $this->beforeQuery(function(self $query) { + $this->applyHandleParam($query); + $this->applyEditableParam($query); + $this->applyRefParam($query); + }); + } + + /** + * Sets the [[$editable]] property. + * + * @param bool|null $value The property value (defaults to true) + * + * @uses $editable + */ + public function editable(?bool $value = true): static + { + $this->editable = $value; + + return $this; + } + + /** + * Narrows the query results based on the global sets’ handles. + * + * Possible values include: + * + * | Value | Fetches global sets… + * | - | - + * | `'foo'` | with a handle of `foo`. + * | `'not foo'` | not with a handle of `foo`. + * | `['foo', 'bar']` | with a handle of `foo` or `bar`. + * | `['not', 'foo', 'bar']` | not with a handle of `foo` or `bar`. + * + * --- + * + * ```twig + * {# Fetch the global set with a handle of 'foo' #} + * {% set {element-var} = {twig-method} + * .handle('foo') + * .one() %} + * ``` + * + * ```php + * // Fetch the global set with a handle of 'foo' + * ${element-var} = {php-method} + * ->handle('foo') + * ->one(); + * ``` + * + * @uses $handle + */ + public function handle(string|array|null $value): static + { + $this->handle = $value; + + return $this; + } + + /** + * Narrows the query results based on a reference string. + */ + public function ref(mixed $value): static + { + $this->ref = $value; + + return $this; + } + + /** + * Applies the 'handle' param to the query being prepared. + */ + private function applyHandleParam(self $query): void + { + if ($query->handle) { + $query->whereParam('globalsets.handle', $query->handle); + } + } + + /** + * Applies the 'editable' param to the query being prepared. + */ + private function applyEditableParam(self $query): void + { + if ($query->editable) { + // Limit the query to only the global sets the user has permission to edit + $query->whereIn('elements.id', Craft::$app->getGlobals()->getEditableSetIds()); + } + } + + /** + * Applies the 'ref' param to the query being prepared. + */ + private function applyRefParam(self $query): void + { + if ($query->ref) { + $query->whereParam('globalsets.handle', $query->ref); + } + } + + /** + * {@inheritdoc} + */ + #[Override] + public function getCacheTags(): array + { + // no need to register cache tags for global set queries, + // unless this is a GraphQL request + if (Route::current()->controller instanceof ApiController) { + return parent::getCacheTags(); + } + + return []; + } +} diff --git a/yii2-adapter/src/Element/Queries/TagQuery.php b/yii2-adapter/src/Element/Queries/TagQuery.php new file mode 100644 index 00000000000..6419e49c0a5 --- /dev/null +++ b/yii2-adapter/src/Element/Queries/TagQuery.php @@ -0,0 +1,73 @@ + + */ +class TagQuery extends ElementQuery +{ + use QueriesTagGroups; + + #[Override] + protected string $table = DeprecatedTable::TAGS; + + #[Override] + protected array $defaultOrderBy = ['elements_sites.title' => SORT_ASC]; + + public function __construct(array $config = []) + { + parent::__construct(Tag::class, $config); + + $this->query->addSelect([ + 'tags.groupId as groupId', + ]); + } + + #[Override] + protected function cacheTags(): array + { + $tags = []; + + if ($this->groupId) { + foreach (Arr::wrap($this->groupId) as $groupId) { + $tags[] = "group:$groupId"; + } + } + + return $tags; + } + + #[Override] + protected function fieldLayouts(): Collection + { + $this->normalizeGroupId($this); + + if ($this->groupId) { + $fieldLayouts = []; + + foreach ($this->groupId as $groupId) { + if ($group = Craft::$app->getTags()->getTagGroupById($groupId)) { + $fieldLayouts[] = $group->getFieldLayout(); + } + } + + return collect($fieldLayouts); + } + + return collect(); + } +} From 8bca161094de39367c41ab051fa455a5be5f74e4 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Fri, 19 Jun 2026 20:50:55 -0700 Subject: [PATCH 7/8] Fixed redirect after creating a new category --- packages/craftcms-legacy/cp/src/js/CategoryIndex.js | 2 +- resources/legacy/cp/dist/cp.js | 2 +- resources/legacy/cp/dist/cp.js.map | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/craftcms-legacy/cp/src/js/CategoryIndex.js b/packages/craftcms-legacy/cp/src/js/CategoryIndex.js index d07f18c6c74..01c35144575 100644 --- a/packages/craftcms-legacy/cp/src/js/CategoryIndex.js +++ b/packages/craftcms-legacy/cp/src/js/CategoryIndex.js @@ -225,7 +225,7 @@ Craft.CategoryIndex = Craft.BaseElementIndex.extend({ }) .then(({data}) => { if (this.settings.context === 'index') { - document.location.href = Craft.getUrl(data.cpEditUrl, {fresh: 1}); + document.location.href = Craft.getUrl(data.element.cpEditUrl, {fresh: 1}); } else { const slideout = Craft.createElementEditor(this.elementType, { siteId: this.siteId, diff --git a/resources/legacy/cp/dist/cp.js b/resources/legacy/cp/dist/cp.js index 2360f13a6d4..980cb518e6a 100644 --- a/resources/legacy/cp/dist/cp.js +++ b/resources/legacy/cp/dist/cp.js @@ -1,2 +1,2 @@ -(function(){var __webpack_modules__={82:function(t,e,i){var s=i(225),n=i(1305),a=i(8543);function r(t){return this instanceof r?(this.nodes=s(t),this):new r(t)}r.prototype.toString=function(){return Array.isArray(this.nodes)?a(this.nodes):""},r.prototype.walk=function(t,e){return n(this.nodes,t,e),this},r.unit=i(4746),r.walk=n,r.stringify=a,t.exports=r},132:function(){class t extends HTMLElement{get items(){return this.querySelectorAll(".sidebar-action")}connectedCallback(){this.trigger=this.querySelector("#sidebar-trigger"),this.trigger&&(this.trigger.addEventListener("open",this.expand.bind(this)),this.trigger.addEventListener("close",this.collapse.bind(this)))}disconnectedCallback(){this.trigger&&(this.trigger.removeEventListener("open",this.expand.bind(this)),this.trigger.removeEventListener("close",this.collapse.bind(this))),this.expand()}itemHasTooltip(t){return t.querySelector("craft-tooltip")}createTooltips(){this.items&&this.items.forEach(t=>{if(this.itemHasTooltip(t))return;const e=document.createElement("craft-tooltip");e.setAttribute("placement","right"),e.setAttribute("trigger",".sidebar-action"),e.setAttribute("text",t.querySelector(".label")?.textContent),t.append(e)})}destroyTooltips(){this.items&&this.items.forEach(t=>{const e=t.querySelector("craft-tooltip");e?.remove()})}expand(){document.body.setAttribute("data-sidebar","expanded"),Craft.setCookie("sidebar","expanded"),this.destroyTooltips()}collapse(){document.body.setAttribute("data-sidebar","collapsed"),Craft.setCookie("sidebar","collapsed"),this.createTooltips()}}customElements.define("craft-global-sidebar",t)},225:function(t){var e="(".charCodeAt(0),i=")".charCodeAt(0),s="'".charCodeAt(0),n='"'.charCodeAt(0),a="\\".charCodeAt(0),r="/".charCodeAt(0),o=",".charCodeAt(0),l=":".charCodeAt(0),h="*".charCodeAt(0),d="u".charCodeAt(0),c="U".charCodeAt(0),u="+".charCodeAt(0),p=/^[a-f0-9?-]+$/i;t.exports=function(t){for(var f,g,m,b,$,C,v,y,w,_=[],S=t,I=0,T=S.charCodeAt(I),x=S.length,E=[{nodes:_}],M=0,k="",A="",P="";I