diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b61847..7e0ef26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,25 @@ -# Changelog - -[Unreleased changes](https://github.com/justbetter/statamic-structured-data/compare/2.3.0...2.3.0) -## [2.3.0](https://github.com/justbetter/statamic-structured-data/releases/tag/2.3.0) - 2026-07-28 - +# Changelog + +[Unreleased changes](https://github.com/justbetter/statamic-structured-data/compare/2.3.0...2.3.0) +## Unreleased + +### Added +- Added optional Runway resource support (templates apply to all models of a resource) +- Added `structured-data:for` tag for explicit item/model rendering + +## [2.3.0](https://github.com/justbetter/statamic-structured-data/releases/tag/2.3.0) - 2026-07-28 + ### Added - Added SEO Pro support (#32) -- Added base package (#31) - -## [2.2.1](https://github.com/justbetter/statamic-structured-data/releases/tag/2.2.1) - 2026-05-05 - +- Added base package (#31) + +## [2.2.1](https://github.com/justbetter/statamic-structured-data/releases/tag/2.2.1) - 2026-05-05 + ### Fixed -- Fixed vite script loading (#29) - -## [2.2.0](https://github.com/justbetter/statamic-structured-data/releases/tag/2.2.0) - 2026-05-05 - +- Fixed vite script loading (#29) + +## [2.2.0](https://github.com/justbetter/statamic-structured-data/releases/tag/2.2.0) - 2026-05-05 + ### Added - Added field duplication (#26) - Added field collapse (#26) @@ -22,16 +28,16 @@ - show imported fieldset fields (#28) ### Fixed -- Parse Bard content correctly (#27) - -## [2.1.0](https://github.com/justbetter/statamic-structured-data/releases/tag/2.1.0) - 2026-04-13 - +- Parse Bard content correctly (#27) + +## [2.1.0](https://github.com/justbetter/statamic-structured-data/releases/tag/2.1.0) - 2026-04-13 + ### Added -- Add Support for Laravel 13 (#24) - -## [2.0.0](https://github.com/justbetter/statamic-structured-data/releases/tag/2.0.0) - 2026-03-24 - +- Add Support for Laravel 13 (#24) + +## [2.0.0](https://github.com/justbetter/statamic-structured-data/releases/tag/2.0.0) - 2026-03-24 + ### Added - Added apply to all action for templates (#19) @@ -40,77 +46,77 @@ - Added tests + Small refactor (#22) ### Changed -- Statamic 6 update + Vue 3 & Statamic UI refactor (#23) - -## [1.5.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.5.0) - 2025-12-15 - +- Statamic 6 update + Vue 3 & Statamic UI refactor (#23) + +## [1.5.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.5.0) - 2025-12-15 + ### Added - Added option to auto assign templates to new entries (#17) -- Changed Phpstan level & fixed Phpstan errors + ran Pint (#18) - -## [1.4.2](https://github.com/justbetter/statamic-structured-data/releases/tag/1.4.2) - 2025-12-05 - +- Changed Phpstan level & fixed Phpstan errors + ran Pint (#18) + +## [1.4.2](https://github.com/justbetter/statamic-structured-data/releases/tag/1.4.2) - 2025-12-05 + ### Fixed -- Fixed object array output (#15) - -## [1.4.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.4.0) - 2025-08-26 - +- Fixed object array output (#15) + +## [1.4.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.4.0) - 2025-08-26 + ### Added -- Presets (#11) - -## [1.3.1](https://github.com/justbetter/statamic-structured-data/releases/tag/1.3.1) - 2025-08-12 - +- Presets (#11) + +## [1.3.1](https://github.com/justbetter/statamic-structured-data/releases/tag/1.3.1) - 2025-08-12 + ### Fixed -- Dark mode styling fix (#10) - -## [1.3.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.3.0) - 2025-05-20 - +- Dark mode styling fix (#10) + +## [1.3.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.3.0) - 2025-05-20 + ### Changed - - Changed injection of structured data (#9) - -## [1.2.2](https://github.com/justbetter/statamic-structured-data/releases/tag/1.2.2) - 2025-05-19 - + - Changed injection of structured data (#9) + +## [1.2.2](https://github.com/justbetter/statamic-structured-data/releases/tag/1.2.2) - 2025-05-19 + ### Changed - - Allow json response from service class (#8) - -## [1.2.1](https://github.com/justbetter/statamic-structured-data/releases/tag/1.2.1) - 2025-05-14 - + - Allow json response from service class (#8) + +## [1.2.1](https://github.com/justbetter/statamic-structured-data/releases/tag/1.2.1) - 2025-05-14 + ### Changed - - Updated readme (#7) - -## [1.2.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.2.0) - 2025-05-14 - + - Updated readme (#7) + +## [1.2.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.2.0) - 2025-05-14 + ### Added - - Added numeric field to builder (#5) - -## [1.1.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.1.0) - 2025-05-12 - + - Added numeric field to builder (#5) + +## [1.1.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.1.0) - 2025-05-12 + ### Added - - Added support for structured data templates on Terms (#4) - -## [1.0.3](https://github.com/justbetter/statamic-structured-data/releases/tag/1.0.3) - 2025-05-09 - + - Added support for structured data templates on Terms (#4) + +## [1.0.3](https://github.com/justbetter/statamic-structured-data/releases/tag/1.0.3) - 2025-05-09 + ### Fixed - Skip collection & taxonomy boot when running in console (#3) - - -## [1.0.2](https://github.com/justbetter/statamic-structured-data/releases/tag/1.0.2) - 2025-05-09 - + + +## [1.0.2](https://github.com/justbetter/statamic-structured-data/releases/tag/1.0.2) - 2025-05-09 + ### Fixed -- Fixed infinite loop when loading variables (#2). - -## [1.0.1](https://github.com/justbetter/statamic-structured-data/releases/tag/1.0.1) - 2025-05-02 - +- Fixed infinite loop when loading variables (#2). + +## [1.0.1](https://github.com/justbetter/statamic-structured-data/releases/tag/1.0.1) - 2025-05-02 + ### Added -- Added Laravel 12 support https://github.com/justbetter/statamic-structured-data/pull/1 - -## [1.0.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.0.0) - 2025-04-25 - -**Initial release**: https://github.com/justbetter/statamic-structured-data/commits/1.0.0 - +- Added Laravel 12 support https://github.com/justbetter/statamic-structured-data/pull/1 + +## [1.0.0](https://github.com/justbetter/statamic-structured-data/releases/tag/1.0.0) - 2025-04-25 + +**Initial release**: https://github.com/justbetter/statamic-structured-data/commits/1.0.0 + diff --git a/README.md b/README.md index 7c60779..f1f87cd 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,13 @@ This Statamic addon provides a powerful and flexible way to add structured data ## Features -- 🔄 Dynamic JSON-LD generation based on entry and term data +- 🔄 Dynamic JSON-LD generation based on entry, term, and Runway model data - 📝 Template-based structured data configuration - 📦 Built-in schema presets (WebSite, WebPage, Organization, Article, LocalBusiness) - 🎯 Support for multiple schemas per page - 🛠 Antlers template parsing support - 🧩 Support for replicator-to-JSON-LD field mapping +- ✈️ Optional [Runway](https://statamic.com/addons/rad-pack/runway) resource support - 💪 Flexible and extensible architecture ## Requirements @@ -57,6 +58,7 @@ After publishing the config, you can configure: - which collections support structured data templates - which taxonomies support structured data objects +- which Runway resource handles should use structured data templates - whether presets are enabled - which default presets are available - custom preset paths @@ -71,11 +73,41 @@ Create templates in your Statamic control panel that define your structured data - Custom fields with various data types (strings, numeric, arrays, objects) - Dynamic values using Antlers templating syntax -### 2. Assigning Templates to Entries +### 2. Assigning Templates to Entries and Terms In your entry or term's content, you can assign one or more structured data templates using the `structured_data_templates` field. The addon will automatically process these templates and generate the appropriate JSON-LD scripts. -### 3. Rendering Structured Data +### 3. Runway resources (optional) + +Runway is soft-optional. When `statamic-rad-pack/runway` is installed, templates can target a Runway resource (`blueprint_type: runway` + `use_for_runway`). Those templates apply to **all** models of that resource at render time — there is no per-model template picker. + +Enable resources in config: + +```php +'runway' => [ + 'product', + 'category', +], +``` + +For projects that use Runway frontend routing, `structured-data:head` resolves the current model via Runway URI lookup. + +For Magento/Rapidez-style routes (or any custom routing), pass the current model explicitly: + +**Blade**: + +```blade +@isset($product) + {!! Statamic::tag('structured-data:for')->param('item', $product)->param('resource', 'product')->fetch() !!} +@endisset +{!! Statamic::tag('structured-data:head')->fetch() !!} +``` + +The optional `resource` param forces the Runway handle when the storefront model class differs from the Runway model class. + +Available variables for Runway templates include blueprint fields plus model attributes/appends. + +### 4. Rendering Structured Data Render the generated JSON-LD where you need it in your layout: @@ -136,4 +168,4 @@ The MIT License (MIT). Please see [License File](LICENSE) for more information. JustBetter logo - \ No newline at end of file + diff --git a/composer.json b/composer.json index 5eeb87a..94a98a6 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,8 @@ }, "autoload-dev": { "psr-4": { - "Justbetter\\StatamicStructuredData\\Tests\\": "tests" + "Justbetter\\StatamicStructuredData\\Tests\\": "tests", + "StatamicRadPack\\Runway\\": "stubs/runway" } }, "require": { @@ -24,6 +25,9 @@ "phpstan/phpstan-mockery": "^2.0", "phpunit/phpunit": "^11.5" }, + "suggest": { + "statamic-rad-pack/runway": "Required to attach structured data templates to Runway resources." + }, "config": { "allow-plugins": { "pixelfear/composer-dist-plugin": true, diff --git a/config/structured-data.php b/config/structured-data.php index ec70d6d..3ec2895 100644 --- a/config/structured-data.php +++ b/config/structured-data.php @@ -9,6 +9,11 @@ // Add taxonomy handles here that should have structured data objects // Example: 'categories', 'tags' ], + 'runway' => [ + // Add Runway resource handles here that should use structured data templates + // Example: 'product', 'category' + // Templates targeting a Runway resource apply to all models of that resource. + ], 'presets' => [ 'enabled' => true, 'default_presets' => ['website', 'organization', 'article', 'webpage', 'localbusiness'], diff --git a/phpstan.neon b/phpstan.neon index adecec8..940263e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,3 +7,7 @@ parameters: - src - tests level: 9 + scanFiles: + - stubs/runway/Runway.php + - stubs/runway/Resource.php + - stubs/runway/ModelRepository.php diff --git a/resources/blueprints/collections/structured_data_templates/structured_data_templates.yaml b/resources/blueprints/collections/structured_data_templates/structured_data_templates.yaml index bda8ccf..3330118 100644 --- a/resources/blueprints/collections/structured_data_templates/structured_data_templates.yaml +++ b/resources/blueprints/collections/structured_data_templates/structured_data_templates.yaml @@ -42,6 +42,9 @@ tabs: - key: taxonomy value: Taxonomy + - + key: runway + value: Runway default: collection type: select display: 'Blueprint type' @@ -70,6 +73,19 @@ tabs: - required if: blueprint_type: 'equals taxonomy' + - + handle: use_for_runway + field: + type: structured_data_runway_resources + display: 'Use for Runway resource' + instructions: 'Templates targeting a Runway resource apply to all models of that resource.' + clearable: true + localizable: true + validate: + - sometimes + - required + if: + blueprint_type: 'equals runway' - handle: apply_automatically field: @@ -78,6 +94,8 @@ tabs: instructions: 'Apply the structured data template automatically to new entries of the selected collection or taxonomy.' localizable: true default: false + unless: + blueprint_type: 'equals runway' - handle: structured_data_available_variables field: diff --git a/resources/dist/build/assets/StructuredDataFieldEditor-CO12Y0iM.js b/resources/dist/build/assets/StructuredDataFieldEditor-BnA2g-9i.js similarity index 99% rename from resources/dist/build/assets/StructuredDataFieldEditor-CO12Y0iM.js rename to resources/dist/build/assets/StructuredDataFieldEditor-BnA2g-9i.js index 69cf8eb..337d3ef 100644 --- a/resources/dist/build/assets/StructuredDataFieldEditor-CO12Y0iM.js +++ b/resources/dist/build/assets/StructuredDataFieldEditor-BnA2g-9i.js @@ -1 +1 @@ -import{_ as T,S as x,P as E,a as B,L as S,I as R,H as L,D as K,C as q,b as w,B as C,r as p,o as n,c as f,d as i,e as o,w as d,f as c,t as r,g as V,h as _,F as O,i as D,j as z,k as H,v as Z,l as G,m as Q,n as W,p as X,u as b}from"./statamic-structured-data-DSPlP4f9.js";const Y={name:"ReplicatorFieldMapper",components:{Button:C,Card:w,Checkbox:q,Description:K,Heading:L,Input:R,Label:S,Panel:B,PanelHeader:E,Select:x},props:{modelValue:{type:Object,default:()=>({replicator_field:"",set:"",mappings:[],flat:!1,flat_key_field:"",flat_value_field:""})},replicatorFields:{type:Array,default:()=>[]}},emits:["update:modelValue"],data(){return{localConfig:this.normalizeConfig(this.modelValue),modeOptions:[{value:"field",label:"From Replicator Field"},{value:"static",label:"Static Value"},{value:"nested_replicator",label:"Nested Replicator"}],confirmRemoveMappingOpen:!1,mappingIndexToRemove:null}},computed:{availableReplicatorFields(){return!this.replicatorFields||!Array.isArray(this.replicatorFields)?[]:this.replicatorFields},replicatorFieldOptions(){return!this.availableReplicatorFields||this.availableReplicatorFields.length===0?[]:this.availableReplicatorFields.map(e=>({value:e.handle,label:e.display||e.handle}))},selectedReplicatorField(){return this.localConfig.replicator_field?this.availableReplicatorFields.find(e=>e.handle===this.localConfig.replicator_field):null},setOptions(){return this.selectedReplicatorField?this.selectedReplicatorField.sets.map(e=>({value:e.value,label:e.label})):[]},flatFieldOptions(){return this.getFieldOptionsForMapping({})}},watch:{localConfig:{deep:!0,handler(e){const a=JSON.stringify(e),u=JSON.stringify(this.modelValue);a!==u&&this.$emit("update:modelValue",JSON.parse(a))}},modelValue:{deep:!0,handler(e){const a=this.normalizeConfig(e),u=JSON.stringify(a),v=JSON.stringify(this.localConfig);u!==v&&(this.localConfig=JSON.parse(u))}}},methods:{normalizeConfig(e){const a={replicator_field:"",set:"",mappings:[],flat:!1,flat_key_field:"",flat_value_field:""};return!e||typeof e!="object"?a:{replicator_field:e.replicator_field||"",set:e.set||"",mappings:Array.isArray(e.mappings)?JSON.parse(JSON.stringify(e.mappings)):[],flat:e.flat===!0,flat_key_field:e.flat_key_field||"",flat_value_field:e.flat_value_field||""}},addMapping(){this.localConfig.mappings.push({key:"",mode:"field",field:"",static:"",nested:{replicator_field:"",set:"",mappings:[]}})},removeMapping(e){this.localConfig.mappings.splice(e,1)},requestRemoveMapping(e){this.mappingIndexToRemove=e,this.confirmRemoveMappingOpen=!0},confirmRemoveMapping(){this.mappingIndexToRemove!==null&&(this.removeMapping(this.mappingIndexToRemove),this.mappingIndexToRemove=null)},sanitizeKey(e){e.key=e.key.replace(/[^a-zA-Z0-9@:-]/g,"")},getFieldOptionsForMapping(e){if(!this.selectedReplicatorField)return[];const a=this.selectedReplicatorField.sets||[];let u=[];if(this.localConfig.set){const t=a.find(m=>m.value===this.localConfig.set);t&&t.fields&&(u=t.fields)}else a.forEach(t=>{t.fields&&(u=u.concat(t.fields))});const v=[],l=new Set;return u.forEach(t=>{l.has(t.value)||(l.add(t.value),v.push(t))}),v}}},$={class:"replicator-mapper space-y-3"},ee={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},le={key:0,class:"text-xs text-yellow-600 mt-1 bg-yellow-50 p-2 rounded"},te={class:"flex flex-col justify-center gap-2"},ae={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},oe={class:"text-xs text-gray-500 mt-1"},ie={class:"text-xs text-gray-500 mt-1"},de={class:"flex items-center justify-between mb-2"},ne={class:"font-semibold text-gray-700"},se={key:0,class:"text-sm text-gray-500"},re={class:"flex items-start gap-2"},ue={class:"flex-1"},me={class:"w-40"},ce={class:"mt-2"};function fe(e,a,u,v,l,t){const m=p("Label"),y=p("Select"),A=p("Checkbox"),U=p("Description"),j=p("PanelHeader"),F=p("Card"),g=p("Panel"),k=p("Button"),N=p("Input"),I=p("replicator-field-mapper",!0),J=p("ConfirmationModal");return n(),f("div",$,[i("div",ee,[i("div",null,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Replicator Field")),1)]),_:1}),o(y,{modelValue:l.localConfig.replicator_field,"onUpdate:modelValue":[a[0]||(a[0]=s=>l.localConfig.replicator_field=s),a[1]||(a[1]=()=>{l.localConfig.set=""})],options:t.replicatorFieldOptions,placeholder:t.replicatorFieldOptions.length>0?e.__("Select replicator field"):e.__("No replicator fields available"),disabled:t.replicatorFieldOptions.length===0},null,8,["modelValue","options","placeholder","disabled"]),t.replicatorFieldOptions.length===0?(n(),f("div",le,[i("strong",null,r(e.__("No replicator fields found.")),1),a[7]||(a[7]=i("br",null,null,-1)),c(" "+r(e.__('Make sure your Structured Data Template has "Use for collection" or "Use for taxonomy" set, and that collection/taxonomy has replicator fields in its blueprint.')),1)])):V("",!0)]),i("div",null,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Limit to Set (optional)")),1)]),_:1}),o(y,{modelValue:l.localConfig.set,"onUpdate:modelValue":a[2]||(a[2]=s=>l.localConfig.set=s),options:t.setOptions,placeholder:e.__("All sets"),clearable:!0},null,8,["modelValue","options","placeholder"])])]),o(g,null,{default:d(()=>[o(j,null,{default:d(()=>[i("div",te,[i("span",null,r(e.__("Flat mode")),1),o(U,null,{default:d(()=>[o(A,{modelValue:l.localConfig.flat,"onUpdate:modelValue":a[3]||(a[3]=s=>l.localConfig.flat=s),label:e.__("Create a flat object where each replicator row becomes a key-value pair."),class:"items-center"},null,8,["modelValue","label"])]),_:1})])]),_:1}),l.localConfig.flat?(n(),_(F,{key:0},{default:d(()=>[i("div",ae,[i("div",null,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Key Field")),1)]),_:1}),o(y,{modelValue:l.localConfig.flat_key_field,"onUpdate:modelValue":a[4]||(a[4]=s=>l.localConfig.flat_key_field=s),options:t.flatFieldOptions,placeholder:e.__("Select field to use as key")},null,8,["modelValue","options","placeholder"]),i("p",oe,r(e.__("Field value will be used as the object key")),1)]),i("div",null,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Value Field")),1)]),_:1}),o(y,{modelValue:l.localConfig.flat_value_field,"onUpdate:modelValue":a[5]||(a[5]=s=>l.localConfig.flat_value_field=s),options:t.flatFieldOptions,placeholder:e.__("Select field to use as value")},null,8,["modelValue","options","placeholder"]),i("p",ie,r(e.__("Field value will be used as the object value")),1)])])]),_:1})):V("",!0)]),_:1}),l.localConfig.flat?V("",!0):(n(),_(F,{key:0,class:"mt-3"},{default:d(()=>[i("div",de,[i("h4",ne,r(e.__("Field Mappings")),1),o(k,{class:"text-sm",onClick:t.addMapping},{default:d(()=>[c(r(e.__("Add Mapping")),1)]),_:1},8,["onClick"])]),l.localConfig.mappings.length?V("",!0):(n(),f("div",se,r(e.__("No mappings yet. Add one to map replicator fields into your JSON-LD object.")),1)),(n(!0),f(O,null,D(l.localConfig.mappings,(s,M)=>(n(),_(F,{key:M,class:"mb-3"},{default:d(()=>[i("div",re,[i("div",ue,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("JSON-LD Key")),1)]),_:1}),o(N,{modelValue:s.key,"onUpdate:modelValue":[h=>s.key=h,()=>t.sanitizeKey(s)],placeholder:"e.g. name"},null,8,["modelValue","onUpdate:modelValue"])]),i("div",me,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Source")),1)]),_:1}),o(y,{modelValue:s.mode,"onUpdate:modelValue":h=>s.mode=h,options:l.modeOptions},null,8,["modelValue","onUpdate:modelValue","options"])]),o(k,{class:"mt-6",variant:"danger",onClick:h=>t.requestRemoveMapping(M)},{default:d(()=>[c(r(e.__("Remove")),1)]),_:1},8,["onClick"])]),i("div",ce,[s.mode==="static"?(n(),f(O,{key:0},[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Static Value")),1)]),_:1}),o(N,{modelValue:s.static,"onUpdate:modelValue":h=>s.static=h,placeholder:e.__("e.g. PropertyValue")},null,8,["modelValue","onUpdate:modelValue","placeholder"])],64)):s.mode==="field"?(n(),f(O,{key:1},[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Replicator Field")),1)]),_:1}),o(y,{modelValue:s.field,"onUpdate:modelValue":h=>s.field=h,options:t.getFieldOptionsForMapping(s),placeholder:e.__("Select field")},null,8,["modelValue","onUpdate:modelValue","options","placeholder"])],64)):s.mode==="nested_replicator"?(n(),_(F,{key:2,class:"mt-2"},{default:d(()=>[o(I,{modelValue:s.nested,"onUpdate:modelValue":h=>s.nested=h,"replicator-fields":u.replicatorFields},null,8,["modelValue","onUpdate:modelValue","replicator-fields"])]),_:2},1024)):V("",!0)])]),_:2},1024))),128))]),_:1})),l.confirmRemoveMappingOpen?(n(),_(J,{key:1,open:l.confirmRemoveMappingOpen,"onUpdate:open":a[6]||(a[6]=s=>l.confirmRemoveMappingOpen=s),"body-text":e.__("Are you sure you want to remove this mapping? This action cannot be undone."),onConfirm:t.confirmRemoveMapping},null,8,["open","body-text","onConfirm"])):V("",!0)])}const pe=T(Y,[["render",fe]]),ye={name:"StructuredDataObject",components:{Button:C,Card:w,ConfirmationModal:z,Input:R,Label:S,StructuredDataFieldEditor:W(()=>X(()=>Promise.resolve().then(()=>xe),void 0))},props:{modelValue:{type:Object,default:()=>({specialProps:{type:"",id:""},fields:[]})},baseUrl:{type:String,required:!0},parentType:{type:String,default:""},fieldKey:{type:String,default:""},replicatorFields:{type:Array,default:()=>[]}},data(){return{objectData:JSON.parse(JSON.stringify(this.modelValue)),confirmRemoveFieldOpen:!1,fieldIndexToRemove:null,confirmRemoveArrayValueOpen:!1,arrayRemoveContext:{field:null,valueIndex:null}}},computed:{suggestedId(){return this.objectData.specialProps.type?`{{ site:url }}/#${this.objectData.specialProps.type}`:""},selectOptions(){return[{value:"string",label:"String"},{value:"numeric",label:"Numeric"},{value:"array",label:"Array"},{value:"object",label:"Object"},{value:"replicator_object_array",label:"Replicator Object Array"}]}},watch:{objectData:{deep:!0,handler(e){const a=JSON.stringify(e),u=JSON.stringify(this.modelValue);a!==u&&this.$emit("update:modelValue",JSON.parse(JSON.stringify(e)))}},"objectData.specialProps.type"(e){!this.objectData.specialProps.id&&e&&this.useDefaultId()},modelValue:{deep:!0,handler(e){const a=JSON.stringify(e),u=JSON.stringify(this.objectData);a!==u&&(this.objectData=JSON.parse(JSON.stringify(e)))}}},methods:{addField(){this.objectData.fields.push({key:"",type:"string",value:"",values:[],fields:[],config:{}})},removeField(e){this.objectData.fields.splice(e,1)},addArrayValue(e){Q(e)},removeArrayValue(e,a){G(e,a)},validateKey(e){Z(e)},useDefaultId(){this.objectData.specialProps.id=this.suggestedId},handleTypeChange(e){H(e)},requestRemoveField(e){this.fieldIndexToRemove=e,this.confirmRemoveFieldOpen=!0},confirmRemoveField(){this.fieldIndexToRemove!==null&&(this.removeField(this.fieldIndexToRemove),this.fieldIndexToRemove=null)},requestRemoveArrayValue(e,a){this.arrayRemoveContext={field:e,valueIndex:a},this.confirmRemoveArrayValueOpen=!0},confirmRemoveArrayValue(){const e=this.arrayRemoveContext;!e||!e.field||e.valueIndex===null||(this.removeArrayValue(e.field,e.valueIndex),this.arrayRemoveContext={field:null,valueIndex:null})}}},ve={class:"mb-4"},be={class:"mb-2"},ge={class:"mb-2"},Ve={class:"flex"};function he(e,a,u,v,l,t){const m=p("Label"),y=p("Input"),A=p("Button"),U=p("StructuredDataFieldEditor"),j=p("ConfirmationModal"),F=p("Card");return n(),_(F,{class:"structured-data-object"},{default:d(()=>[i("div",ve,[i("div",be,[o(m,{class:"mb-1.5"},{default:d(()=>[...a[4]||(a[4]=[c("@type",-1)])]),_:1}),o(y,{modelValue:l.objectData.specialProps.type,"onUpdate:modelValue":a[0]||(a[0]=g=>l.objectData.specialProps.type=g),placeholder:"e.g. Organization, Product, BlogPosting"},null,8,["modelValue"])]),i("div",ge,[o(m,{class:"mb-1.5"},{default:d(()=>[...a[5]||(a[5]=[c("@id",-1)])]),_:1}),i("div",Ve,[o(y,{modelValue:l.objectData.specialProps.id,"onUpdate:modelValue":a[1]||(a[1]=g=>l.objectData.specialProps.id=g),class:"flex-1",placeholder:t.suggestedId},null,8,["modelValue","placeholder"]),o(A,{class:"ml-2",onClick:t.useDefaultId},{default:d(()=>[c(r(e.__("Use Default")),1)]),_:1},8,["onClick"])])])]),i("div",null,[(n(!0),f(O,null,D(l.objectData.fields,(g,k)=>(n(),f("div",{key:k,class:"mb-4 border-b pb-4"},[o(U,{field:g,"select-options":t.selectOptions,"base-url":u.baseUrl,"replicator-fields":u.replicatorFields,"remove-field-label":e.__("Remove Property"),onValidateKey:t.validateKey,onTypeChange:t.handleTypeChange,onAddArrayValue:t.addArrayValue,onRemoveArrayValue:t.requestRemoveArrayValue,onRemoveField:N=>t.requestRemoveField(k)},null,8,["field","select-options","base-url","replicator-fields","remove-field-label","onValidateKey","onTypeChange","onAddArrayValue","onRemoveArrayValue","onRemoveField"])]))),128)),o(A,{onClick:t.addField,variant:"primary"},{default:d(()=>[c(r(e.__("Add Property")),1)]),_:1},8,["onClick"])]),l.confirmRemoveFieldOpen?(n(),_(j,{key:0,open:l.confirmRemoveFieldOpen,"onUpdate:open":a[2]||(a[2]=g=>l.confirmRemoveFieldOpen=g),"body-text":e.__("Are you sure you want to remove this field? This action cannot be undone."),onConfirm:t.confirmRemoveField},null,8,["open","body-text","onConfirm"])):V("",!0),l.confirmRemoveArrayValueOpen?(n(),_(j,{key:1,open:l.confirmRemoveArrayValueOpen,"onUpdate:open":a[3]||(a[3]=g=>l.confirmRemoveArrayValueOpen=g),"body-text":e.__("Are you sure you want to remove this value? This action cannot be undone."),onConfirm:t.confirmRemoveArrayValue},null,8,["open","body-text","onConfirm"])):V("",!0)]),_:1})}const P=T(ye,[["render",he]]),_e={class:"grid grid-cols-1 md:grid-cols-3 gap-3"},Ce={class:"col-span-2"},Fe={class:"mt-3"},Re={key:2,class:"mt-2"},Oe={class:"flex flex-col gap-2 space-y-2"},ke={key:3,class:"mt-2"},Se={key:4,class:"mt-2"},Ae={class:"flex flex-col gap-2 space-y-2"},je={key:5,class:"mt-2"},De={key:6,class:"mt-2"},Ue={key:0,class:"flex justify-end mt-3"},Ne={__name:"StructuredDataFieldEditor",props:{field:{type:Object,required:!0},selectOptions:{type:Array,required:!0},taxonomyTermOptions:{type:Array,default:()=>[]},baseUrl:{type:String,default:""},replicatorFields:{type:Array,default:()=>[]},objectArrayEnabled:{type:Boolean,default:!1},dataObjectEnabled:{type:Boolean,default:!1},showRemoveButton:{type:Boolean,default:!0},removeFieldLabel:{type:String,default:"Remove Field"}},emits:["validate-key","type-change","add-array-value","remove-array-value","remove-field"],setup(e,{emit:a}){const u=a;return(v,l)=>(n(),f("div",null,[i("div",_e,[i("div",Ce,[o(b(S),{class:"mb-1.5"},{default:d(()=>[c(r(v.__("Key")),1)]),_:1}),o(b(R),{modelValue:e.field.key,"onUpdate:modelValue":[l[0]||(l[0]=t=>e.field.key=t),l[1]||(l[1]=t=>u("validate-key",e.field))]},null,8,["modelValue"])]),i("div",null,[o(b(S),{class:"mb-1.5"},{default:d(()=>[c(r(v.__("Type")),1)]),_:1}),o(b(x),{modelValue:e.field.type,"onUpdate:modelValue":[l[2]||(l[2]=t=>e.field.type=t),l[3]||(l[3]=t=>u("type-change",e.field))],options:e.selectOptions},null,8,["modelValue","options"])])]),i("div",Fe,[o(b(S),{class:"mb-1.5"},{default:d(()=>[c(r(v.__("Value")),1)]),_:1}),e.field.type==="string"?(n(),_(b(R),{key:0,modelValue:e.field.value,"onUpdate:modelValue":l[4]||(l[4]=t=>e.field.value=t),placeholder:"Enter value"},null,8,["modelValue"])):V("",!0),e.field.type==="numeric"?(n(),_(b(R),{key:1,type:"number",modelValue:e.field.value,"onUpdate:modelValue":l[5]||(l[5]=t=>e.field.value=t),placeholder:"Enter value"},null,8,["modelValue"])):e.field.type==="array"?(n(),f("div",Re,[i("div",Oe,[(n(!0),f(O,null,D(e.field.values,(t,m)=>(n(),f("div",{key:m,class:"flex items-center gap-2"},[o(b(R),{modelValue:e.field.values[m],"onUpdate:modelValue":y=>e.field.values[m]=y},null,8,["modelValue","onUpdate:modelValue"]),o(b(C),{onClick:y=>u("remove-array-value",e.field,m),class:"inline-flex items-center px-2 py-1",variant:"danger"},{default:d(()=>[i("span",null,r(v.__("Remove")),1)]),_:1},8,["onClick"])]))),128))]),o(b(C),{onClick:l[6]||(l[6]=t=>u("add-array-value",e.field)),variant:"primary",class:"mt-2 text-sm"},{default:d(()=>[c(r(v.__("Add Value")),1)]),_:1})])):e.field.type==="object"?(n(),f("div",ke,[o(P,{modelValue:e.field.value,"onUpdate:modelValue":l[7]||(l[7]=t=>e.field.value=t),"base-url":e.baseUrl,"field-key":e.field.key,"replicator-fields":e.replicatorFields},null,8,["modelValue","base-url","field-key","replicator-fields"])])):e.objectArrayEnabled&&e.field.type==="object_array"?(n(),f("div",Se,[i("div",Ae,[(n(!0),f(O,null,D(e.field.values,(t,m)=>(n(),f("div",{key:m,class:"flex flex-col gap-2"},[o(P,{modelValue:e.field.values[m],"onUpdate:modelValue":y=>e.field.values[m]=y,"base-url":e.baseUrl},null,8,["modelValue","onUpdate:modelValue","base-url"]),i("div",null,[o(b(C),{onClick:y=>u("remove-array-value",e.field,m),class:"inline-flex self-end items-center px-2 py-1",variant:"danger"},{default:d(()=>[i("span",null,r(v.__("Remove")),1)]),_:1},8,["onClick"])])]))),128))]),o(b(C),{onClick:l[8]||(l[8]=t=>u("add-array-value",e.field)),class:"mt-2 text-sm",variant:"primary"},{default:d(()=>[c(r(v.__("Add Value")),1)]),_:1})])):e.dataObjectEnabled&&e.field.type==="data_object"?(n(),f("div",je,[o(b(x),{modelValue:e.field.value,"onUpdate:modelValue":l[9]||(l[9]=t=>e.field.value=t),options:e.taxonomyTermOptions},null,8,["modelValue","options"])])):e.field.type==="replicator_object_array"?(n(),f("div",De,[o(pe,{modelValue:e.field.config,"onUpdate:modelValue":l[10]||(l[10]=t=>e.field.config=t),"base-url":e.baseUrl,"replicator-fields":e.replicatorFields},null,8,["modelValue","base-url","replicator-fields"])])):V("",!0)]),e.showRemoveButton?(n(),f("div",Ue,[o(b(C),{variant:"danger",onClick:l[11]||(l[11]=t=>u("remove-field"))},{default:d(()=>[c(r(e.removeFieldLabel),1)]),_:1})])):V("",!0)]))}},xe=Object.freeze(Object.defineProperty({__proto__:null,default:Ne},Symbol.toStringTag,{value:"Module"}));export{Ne as default}; +import{_ as T,S as x,P as E,a as B,L as S,I as R,H as L,D as K,C as q,b as w,B as C,r as p,o as n,c as f,d as i,e as o,w as d,f as c,t as r,g as V,h as _,F as O,i as D,j as z,k as H,v as Z,l as G,m as Q,n as W,p as X,u as b}from"./statamic-structured-data-BmgHG2xH.js";const Y={name:"ReplicatorFieldMapper",components:{Button:C,Card:w,Checkbox:q,Description:K,Heading:L,Input:R,Label:S,Panel:B,PanelHeader:E,Select:x},props:{modelValue:{type:Object,default:()=>({replicator_field:"",set:"",mappings:[],flat:!1,flat_key_field:"",flat_value_field:""})},replicatorFields:{type:Array,default:()=>[]}},emits:["update:modelValue"],data(){return{localConfig:this.normalizeConfig(this.modelValue),modeOptions:[{value:"field",label:"From Replicator Field"},{value:"static",label:"Static Value"},{value:"nested_replicator",label:"Nested Replicator"}],confirmRemoveMappingOpen:!1,mappingIndexToRemove:null}},computed:{availableReplicatorFields(){return!this.replicatorFields||!Array.isArray(this.replicatorFields)?[]:this.replicatorFields},replicatorFieldOptions(){return!this.availableReplicatorFields||this.availableReplicatorFields.length===0?[]:this.availableReplicatorFields.map(e=>({value:e.handle,label:e.display||e.handle}))},selectedReplicatorField(){return this.localConfig.replicator_field?this.availableReplicatorFields.find(e=>e.handle===this.localConfig.replicator_field):null},setOptions(){return this.selectedReplicatorField?this.selectedReplicatorField.sets.map(e=>({value:e.value,label:e.label})):[]},flatFieldOptions(){return this.getFieldOptionsForMapping({})}},watch:{localConfig:{deep:!0,handler(e){const a=JSON.stringify(e),u=JSON.stringify(this.modelValue);a!==u&&this.$emit("update:modelValue",JSON.parse(a))}},modelValue:{deep:!0,handler(e){const a=this.normalizeConfig(e),u=JSON.stringify(a),v=JSON.stringify(this.localConfig);u!==v&&(this.localConfig=JSON.parse(u))}}},methods:{normalizeConfig(e){const a={replicator_field:"",set:"",mappings:[],flat:!1,flat_key_field:"",flat_value_field:""};return!e||typeof e!="object"?a:{replicator_field:e.replicator_field||"",set:e.set||"",mappings:Array.isArray(e.mappings)?JSON.parse(JSON.stringify(e.mappings)):[],flat:e.flat===!0,flat_key_field:e.flat_key_field||"",flat_value_field:e.flat_value_field||""}},addMapping(){this.localConfig.mappings.push({key:"",mode:"field",field:"",static:"",nested:{replicator_field:"",set:"",mappings:[]}})},removeMapping(e){this.localConfig.mappings.splice(e,1)},requestRemoveMapping(e){this.mappingIndexToRemove=e,this.confirmRemoveMappingOpen=!0},confirmRemoveMapping(){this.mappingIndexToRemove!==null&&(this.removeMapping(this.mappingIndexToRemove),this.mappingIndexToRemove=null)},sanitizeKey(e){e.key=e.key.replace(/[^a-zA-Z0-9@:-]/g,"")},getFieldOptionsForMapping(e){if(!this.selectedReplicatorField)return[];const a=this.selectedReplicatorField.sets||[];let u=[];if(this.localConfig.set){const t=a.find(m=>m.value===this.localConfig.set);t&&t.fields&&(u=t.fields)}else a.forEach(t=>{t.fields&&(u=u.concat(t.fields))});const v=[],l=new Set;return u.forEach(t=>{l.has(t.value)||(l.add(t.value),v.push(t))}),v}}},$={class:"replicator-mapper space-y-3"},ee={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},le={key:0,class:"text-xs text-yellow-600 mt-1 bg-yellow-50 p-2 rounded"},te={class:"flex flex-col justify-center gap-2"},ae={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},oe={class:"text-xs text-gray-500 mt-1"},ie={class:"text-xs text-gray-500 mt-1"},de={class:"flex items-center justify-between mb-2"},ne={class:"font-semibold text-gray-700"},se={key:0,class:"text-sm text-gray-500"},re={class:"flex items-start gap-2"},ue={class:"flex-1"},me={class:"w-40"},ce={class:"mt-2"};function fe(e,a,u,v,l,t){const m=p("Label"),y=p("Select"),A=p("Checkbox"),U=p("Description"),j=p("PanelHeader"),F=p("Card"),g=p("Panel"),k=p("Button"),N=p("Input"),I=p("replicator-field-mapper",!0),J=p("ConfirmationModal");return n(),f("div",$,[i("div",ee,[i("div",null,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Replicator Field")),1)]),_:1}),o(y,{modelValue:l.localConfig.replicator_field,"onUpdate:modelValue":[a[0]||(a[0]=s=>l.localConfig.replicator_field=s),a[1]||(a[1]=()=>{l.localConfig.set=""})],options:t.replicatorFieldOptions,placeholder:t.replicatorFieldOptions.length>0?e.__("Select replicator field"):e.__("No replicator fields available"),disabled:t.replicatorFieldOptions.length===0},null,8,["modelValue","options","placeholder","disabled"]),t.replicatorFieldOptions.length===0?(n(),f("div",le,[i("strong",null,r(e.__("No replicator fields found.")),1),a[7]||(a[7]=i("br",null,null,-1)),c(" "+r(e.__('Make sure your Structured Data Template has "Use for collection" or "Use for taxonomy" set, and that collection/taxonomy has replicator fields in its blueprint.')),1)])):V("",!0)]),i("div",null,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Limit to Set (optional)")),1)]),_:1}),o(y,{modelValue:l.localConfig.set,"onUpdate:modelValue":a[2]||(a[2]=s=>l.localConfig.set=s),options:t.setOptions,placeholder:e.__("All sets"),clearable:!0},null,8,["modelValue","options","placeholder"])])]),o(g,null,{default:d(()=>[o(j,null,{default:d(()=>[i("div",te,[i("span",null,r(e.__("Flat mode")),1),o(U,null,{default:d(()=>[o(A,{modelValue:l.localConfig.flat,"onUpdate:modelValue":a[3]||(a[3]=s=>l.localConfig.flat=s),label:e.__("Create a flat object where each replicator row becomes a key-value pair."),class:"items-center"},null,8,["modelValue","label"])]),_:1})])]),_:1}),l.localConfig.flat?(n(),_(F,{key:0},{default:d(()=>[i("div",ae,[i("div",null,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Key Field")),1)]),_:1}),o(y,{modelValue:l.localConfig.flat_key_field,"onUpdate:modelValue":a[4]||(a[4]=s=>l.localConfig.flat_key_field=s),options:t.flatFieldOptions,placeholder:e.__("Select field to use as key")},null,8,["modelValue","options","placeholder"]),i("p",oe,r(e.__("Field value will be used as the object key")),1)]),i("div",null,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Value Field")),1)]),_:1}),o(y,{modelValue:l.localConfig.flat_value_field,"onUpdate:modelValue":a[5]||(a[5]=s=>l.localConfig.flat_value_field=s),options:t.flatFieldOptions,placeholder:e.__("Select field to use as value")},null,8,["modelValue","options","placeholder"]),i("p",ie,r(e.__("Field value will be used as the object value")),1)])])]),_:1})):V("",!0)]),_:1}),l.localConfig.flat?V("",!0):(n(),_(F,{key:0,class:"mt-3"},{default:d(()=>[i("div",de,[i("h4",ne,r(e.__("Field Mappings")),1),o(k,{class:"text-sm",onClick:t.addMapping},{default:d(()=>[c(r(e.__("Add Mapping")),1)]),_:1},8,["onClick"])]),l.localConfig.mappings.length?V("",!0):(n(),f("div",se,r(e.__("No mappings yet. Add one to map replicator fields into your JSON-LD object.")),1)),(n(!0),f(O,null,D(l.localConfig.mappings,(s,M)=>(n(),_(F,{key:M,class:"mb-3"},{default:d(()=>[i("div",re,[i("div",ue,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("JSON-LD Key")),1)]),_:1}),o(N,{modelValue:s.key,"onUpdate:modelValue":[h=>s.key=h,()=>t.sanitizeKey(s)],placeholder:"e.g. name"},null,8,["modelValue","onUpdate:modelValue"])]),i("div",me,[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Source")),1)]),_:1}),o(y,{modelValue:s.mode,"onUpdate:modelValue":h=>s.mode=h,options:l.modeOptions},null,8,["modelValue","onUpdate:modelValue","options"])]),o(k,{class:"mt-6",variant:"danger",onClick:h=>t.requestRemoveMapping(M)},{default:d(()=>[c(r(e.__("Remove")),1)]),_:1},8,["onClick"])]),i("div",ce,[s.mode==="static"?(n(),f(O,{key:0},[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Static Value")),1)]),_:1}),o(N,{modelValue:s.static,"onUpdate:modelValue":h=>s.static=h,placeholder:e.__("e.g. PropertyValue")},null,8,["modelValue","onUpdate:modelValue","placeholder"])],64)):s.mode==="field"?(n(),f(O,{key:1},[o(m,{class:"mb-1.5"},{default:d(()=>[c(r(e.__("Replicator Field")),1)]),_:1}),o(y,{modelValue:s.field,"onUpdate:modelValue":h=>s.field=h,options:t.getFieldOptionsForMapping(s),placeholder:e.__("Select field")},null,8,["modelValue","onUpdate:modelValue","options","placeholder"])],64)):s.mode==="nested_replicator"?(n(),_(F,{key:2,class:"mt-2"},{default:d(()=>[o(I,{modelValue:s.nested,"onUpdate:modelValue":h=>s.nested=h,"replicator-fields":u.replicatorFields},null,8,["modelValue","onUpdate:modelValue","replicator-fields"])]),_:2},1024)):V("",!0)])]),_:2},1024))),128))]),_:1})),l.confirmRemoveMappingOpen?(n(),_(J,{key:1,open:l.confirmRemoveMappingOpen,"onUpdate:open":a[6]||(a[6]=s=>l.confirmRemoveMappingOpen=s),"body-text":e.__("Are you sure you want to remove this mapping? This action cannot be undone."),onConfirm:t.confirmRemoveMapping},null,8,["open","body-text","onConfirm"])):V("",!0)])}const pe=T(Y,[["render",fe]]),ye={name:"StructuredDataObject",components:{Button:C,Card:w,ConfirmationModal:z,Input:R,Label:S,StructuredDataFieldEditor:W(()=>X(()=>Promise.resolve().then(()=>xe),void 0))},props:{modelValue:{type:Object,default:()=>({specialProps:{type:"",id:""},fields:[]})},baseUrl:{type:String,required:!0},parentType:{type:String,default:""},fieldKey:{type:String,default:""},replicatorFields:{type:Array,default:()=>[]}},data(){return{objectData:JSON.parse(JSON.stringify(this.modelValue)),confirmRemoveFieldOpen:!1,fieldIndexToRemove:null,confirmRemoveArrayValueOpen:!1,arrayRemoveContext:{field:null,valueIndex:null}}},computed:{suggestedId(){return this.objectData.specialProps.type?`{{ site:url }}/#${this.objectData.specialProps.type}`:""},selectOptions(){return[{value:"string",label:"String"},{value:"numeric",label:"Numeric"},{value:"array",label:"Array"},{value:"object",label:"Object"},{value:"replicator_object_array",label:"Replicator Object Array"}]}},watch:{objectData:{deep:!0,handler(e){const a=JSON.stringify(e),u=JSON.stringify(this.modelValue);a!==u&&this.$emit("update:modelValue",JSON.parse(JSON.stringify(e)))}},"objectData.specialProps.type"(e){!this.objectData.specialProps.id&&e&&this.useDefaultId()},modelValue:{deep:!0,handler(e){const a=JSON.stringify(e),u=JSON.stringify(this.objectData);a!==u&&(this.objectData=JSON.parse(JSON.stringify(e)))}}},methods:{addField(){this.objectData.fields.push({key:"",type:"string",value:"",values:[],fields:[],config:{}})},removeField(e){this.objectData.fields.splice(e,1)},addArrayValue(e){Q(e)},removeArrayValue(e,a){G(e,a)},validateKey(e){Z(e)},useDefaultId(){this.objectData.specialProps.id=this.suggestedId},handleTypeChange(e){H(e)},requestRemoveField(e){this.fieldIndexToRemove=e,this.confirmRemoveFieldOpen=!0},confirmRemoveField(){this.fieldIndexToRemove!==null&&(this.removeField(this.fieldIndexToRemove),this.fieldIndexToRemove=null)},requestRemoveArrayValue(e,a){this.arrayRemoveContext={field:e,valueIndex:a},this.confirmRemoveArrayValueOpen=!0},confirmRemoveArrayValue(){const e=this.arrayRemoveContext;!e||!e.field||e.valueIndex===null||(this.removeArrayValue(e.field,e.valueIndex),this.arrayRemoveContext={field:null,valueIndex:null})}}},ve={class:"mb-4"},be={class:"mb-2"},ge={class:"mb-2"},Ve={class:"flex"};function he(e,a,u,v,l,t){const m=p("Label"),y=p("Input"),A=p("Button"),U=p("StructuredDataFieldEditor"),j=p("ConfirmationModal"),F=p("Card");return n(),_(F,{class:"structured-data-object"},{default:d(()=>[i("div",ve,[i("div",be,[o(m,{class:"mb-1.5"},{default:d(()=>[...a[4]||(a[4]=[c("@type",-1)])]),_:1}),o(y,{modelValue:l.objectData.specialProps.type,"onUpdate:modelValue":a[0]||(a[0]=g=>l.objectData.specialProps.type=g),placeholder:"e.g. Organization, Product, BlogPosting"},null,8,["modelValue"])]),i("div",ge,[o(m,{class:"mb-1.5"},{default:d(()=>[...a[5]||(a[5]=[c("@id",-1)])]),_:1}),i("div",Ve,[o(y,{modelValue:l.objectData.specialProps.id,"onUpdate:modelValue":a[1]||(a[1]=g=>l.objectData.specialProps.id=g),class:"flex-1",placeholder:t.suggestedId},null,8,["modelValue","placeholder"]),o(A,{class:"ml-2",onClick:t.useDefaultId},{default:d(()=>[c(r(e.__("Use Default")),1)]),_:1},8,["onClick"])])])]),i("div",null,[(n(!0),f(O,null,D(l.objectData.fields,(g,k)=>(n(),f("div",{key:k,class:"mb-4 border-b pb-4"},[o(U,{field:g,"select-options":t.selectOptions,"base-url":u.baseUrl,"replicator-fields":u.replicatorFields,"remove-field-label":e.__("Remove Property"),onValidateKey:t.validateKey,onTypeChange:t.handleTypeChange,onAddArrayValue:t.addArrayValue,onRemoveArrayValue:t.requestRemoveArrayValue,onRemoveField:N=>t.requestRemoveField(k)},null,8,["field","select-options","base-url","replicator-fields","remove-field-label","onValidateKey","onTypeChange","onAddArrayValue","onRemoveArrayValue","onRemoveField"])]))),128)),o(A,{onClick:t.addField,variant:"primary"},{default:d(()=>[c(r(e.__("Add Property")),1)]),_:1},8,["onClick"])]),l.confirmRemoveFieldOpen?(n(),_(j,{key:0,open:l.confirmRemoveFieldOpen,"onUpdate:open":a[2]||(a[2]=g=>l.confirmRemoveFieldOpen=g),"body-text":e.__("Are you sure you want to remove this field? This action cannot be undone."),onConfirm:t.confirmRemoveField},null,8,["open","body-text","onConfirm"])):V("",!0),l.confirmRemoveArrayValueOpen?(n(),_(j,{key:1,open:l.confirmRemoveArrayValueOpen,"onUpdate:open":a[3]||(a[3]=g=>l.confirmRemoveArrayValueOpen=g),"body-text":e.__("Are you sure you want to remove this value? This action cannot be undone."),onConfirm:t.confirmRemoveArrayValue},null,8,["open","body-text","onConfirm"])):V("",!0)]),_:1})}const P=T(ye,[["render",he]]),_e={class:"grid grid-cols-1 md:grid-cols-3 gap-3"},Ce={class:"col-span-2"},Fe={class:"mt-3"},Re={key:2,class:"mt-2"},Oe={class:"flex flex-col gap-2 space-y-2"},ke={key:3,class:"mt-2"},Se={key:4,class:"mt-2"},Ae={class:"flex flex-col gap-2 space-y-2"},je={key:5,class:"mt-2"},De={key:6,class:"mt-2"},Ue={key:0,class:"flex justify-end mt-3"},Ne={__name:"StructuredDataFieldEditor",props:{field:{type:Object,required:!0},selectOptions:{type:Array,required:!0},taxonomyTermOptions:{type:Array,default:()=>[]},baseUrl:{type:String,default:""},replicatorFields:{type:Array,default:()=>[]},objectArrayEnabled:{type:Boolean,default:!1},dataObjectEnabled:{type:Boolean,default:!1},showRemoveButton:{type:Boolean,default:!0},removeFieldLabel:{type:String,default:"Remove Field"}},emits:["validate-key","type-change","add-array-value","remove-array-value","remove-field"],setup(e,{emit:a}){const u=a;return(v,l)=>(n(),f("div",null,[i("div",_e,[i("div",Ce,[o(b(S),{class:"mb-1.5"},{default:d(()=>[c(r(v.__("Key")),1)]),_:1}),o(b(R),{modelValue:e.field.key,"onUpdate:modelValue":[l[0]||(l[0]=t=>e.field.key=t),l[1]||(l[1]=t=>u("validate-key",e.field))]},null,8,["modelValue"])]),i("div",null,[o(b(S),{class:"mb-1.5"},{default:d(()=>[c(r(v.__("Type")),1)]),_:1}),o(b(x),{modelValue:e.field.type,"onUpdate:modelValue":[l[2]||(l[2]=t=>e.field.type=t),l[3]||(l[3]=t=>u("type-change",e.field))],options:e.selectOptions},null,8,["modelValue","options"])])]),i("div",Fe,[o(b(S),{class:"mb-1.5"},{default:d(()=>[c(r(v.__("Value")),1)]),_:1}),e.field.type==="string"?(n(),_(b(R),{key:0,modelValue:e.field.value,"onUpdate:modelValue":l[4]||(l[4]=t=>e.field.value=t),placeholder:"Enter value"},null,8,["modelValue"])):V("",!0),e.field.type==="numeric"?(n(),_(b(R),{key:1,type:"number",modelValue:e.field.value,"onUpdate:modelValue":l[5]||(l[5]=t=>e.field.value=t),placeholder:"Enter value"},null,8,["modelValue"])):e.field.type==="array"?(n(),f("div",Re,[i("div",Oe,[(n(!0),f(O,null,D(e.field.values,(t,m)=>(n(),f("div",{key:m,class:"flex items-center gap-2"},[o(b(R),{modelValue:e.field.values[m],"onUpdate:modelValue":y=>e.field.values[m]=y},null,8,["modelValue","onUpdate:modelValue"]),o(b(C),{onClick:y=>u("remove-array-value",e.field,m),class:"inline-flex items-center px-2 py-1",variant:"danger"},{default:d(()=>[i("span",null,r(v.__("Remove")),1)]),_:1},8,["onClick"])]))),128))]),o(b(C),{onClick:l[6]||(l[6]=t=>u("add-array-value",e.field)),variant:"primary",class:"mt-2 text-sm"},{default:d(()=>[c(r(v.__("Add Value")),1)]),_:1})])):e.field.type==="object"?(n(),f("div",ke,[o(P,{modelValue:e.field.value,"onUpdate:modelValue":l[7]||(l[7]=t=>e.field.value=t),"base-url":e.baseUrl,"field-key":e.field.key,"replicator-fields":e.replicatorFields},null,8,["modelValue","base-url","field-key","replicator-fields"])])):e.objectArrayEnabled&&e.field.type==="object_array"?(n(),f("div",Se,[i("div",Ae,[(n(!0),f(O,null,D(e.field.values,(t,m)=>(n(),f("div",{key:m,class:"flex flex-col gap-2"},[o(P,{modelValue:e.field.values[m],"onUpdate:modelValue":y=>e.field.values[m]=y,"base-url":e.baseUrl},null,8,["modelValue","onUpdate:modelValue","base-url"]),i("div",null,[o(b(C),{onClick:y=>u("remove-array-value",e.field,m),class:"inline-flex self-end items-center px-2 py-1",variant:"danger"},{default:d(()=>[i("span",null,r(v.__("Remove")),1)]),_:1},8,["onClick"])])]))),128))]),o(b(C),{onClick:l[8]||(l[8]=t=>u("add-array-value",e.field)),class:"mt-2 text-sm",variant:"primary"},{default:d(()=>[c(r(v.__("Add Value")),1)]),_:1})])):e.dataObjectEnabled&&e.field.type==="data_object"?(n(),f("div",je,[o(b(x),{modelValue:e.field.value,"onUpdate:modelValue":l[9]||(l[9]=t=>e.field.value=t),options:e.taxonomyTermOptions},null,8,["modelValue","options"])])):e.field.type==="replicator_object_array"?(n(),f("div",De,[o(pe,{modelValue:e.field.config,"onUpdate:modelValue":l[10]||(l[10]=t=>e.field.config=t),"base-url":e.baseUrl,"replicator-fields":e.replicatorFields},null,8,["modelValue","base-url","replicator-fields"])])):V("",!0)]),e.showRemoveButton?(n(),f("div",Ue,[o(b(C),{variant:"danger",onClick:l[11]||(l[11]=t=>u("remove-field"))},{default:d(()=>[c(r(e.removeFieldLabel),1)]),_:1})])):V("",!0)]))}},xe=Object.freeze(Object.defineProperty({__proto__:null,default:Ne},Symbol.toStringTag,{value:"Module"}));export{Ne as default}; diff --git a/resources/dist/build/assets/statamic-structured-data-DSPlP4f9.js b/resources/dist/build/assets/statamic-structured-data-BmgHG2xH.js similarity index 73% rename from resources/dist/build/assets/statamic-structured-data-DSPlP4f9.js rename to resources/dist/build/assets/statamic-structured-data-BmgHG2xH.js index 0362870..b4b38d1 100644 --- a/resources/dist/build/assets/statamic-structured-data-DSPlP4f9.js +++ b/resources/dist/build/assets/statamic-structured-data-BmgHG2xH.js @@ -1,11 +1,11 @@ -const wn="modulepreload",Fn=function(o){return"/build/"+o},Yr={},un=function(e,n,s){let r=Promise.resolve();if(n&&n.length>0){let t=function(l){return Promise.all(l.map(c=>Promise.resolve(c).then(f=>({status:"fulfilled",value:f}),f=>({status:"rejected",reason:f}))))};document.getElementsByTagName("link");const a=document.querySelector("meta[property=csp-nonce]"),i=(a==null?void 0:a.nonce)||(a==null?void 0:a.getAttribute("nonce"));r=t(n.map(l=>{if(l=Fn(l),l in Yr)return;Yr[l]=!0;const c=l.endsWith(".css"),f=c?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${l}"]${f}`))return;const d=document.createElement("link");if(d.rel=c?"stylesheet":wn,c||(d.as="script"),d.crossOrigin="",d.href=l,i&&d.setAttribute("nonce",i),document.head.appendChild(d),c)return new Promise((v,p)=>{d.addEventListener("load",v),d.addEventListener("error",()=>p(new Error(`Unable to preload CSS for ${l}`)))})}))}function u(t){const a=new Event("vite:preloadError",{cancelable:!0});if(a.payload=t,window.dispatchEvent(a),!a.defaultPrevented)throw t}return r.then(t=>{for(const a of t||[])a.status==="rejected"&&u(a.reason);return e().catch(u)})},cn=window.Vue,{BaseTransition:jn,BaseTransitionPropsValidators:Mn,Comment:Nn,DeprecationTypes:Ln,EffectScope:Un,ErrorCodes:Bn,ErrorTypeStrings:Gn,Fragment:ce,KeepAlive:Vn,ReactiveEffect:Kn,Static:Hn,Suspense:Wn,Teleport:Xn,Text:Yn,TrackOpTypes:zn,Transition:Jn,TransitionGroup:Zn,TriggerOpTypes:Qn,VueElement:kn,__esModule:qn,assertNumber:_n,callWithAsyncErrorHandling:to,callWithErrorHandling:eo,camelize:ro,capitalize:no,cloneVNode:oo,compatUtils:ao,compile:io,computed:Nt,createApp:so,createBlock:Xt,createCommentVNode:Vt,createElementBlock:st,createElementVNode:B,createHydrationRenderer:lo,createPropsRestProxy:uo,createRenderer:co,createSSRApp:fo,createSlots:vo,createStaticVNode:po,createTextVNode:It,createVNode:J,customRef:ho,defineAsyncComponent:Vr,defineComponent:go,defineCustomElement:mo,defineEmits:yo,defineExpose:bo,defineModel:So,defineOptions:xo,defineProps:Eo,defineSSRCustomElement:Oo,defineSlots:Po,devtools:Co,effect:To,effectScope:Io,getCurrentInstance:fn,getCurrentScope:Ao,getCurrentWatcher:Do,getTransitionRawChildren:Ro,guardReactiveProps:$o,h:wo,handleError:Fo,hasInjectionContext:jo,hydrate:Mo,hydrateOnIdle:No,hydrateOnInteraction:Lo,hydrateOnMediaQuery:Uo,hydrateOnVisible:Bo,initCustomFormatter:Go,initDirectivesForSSR:Vo,inject:Ko,isMemoSame:Ho,isProxy:Wo,isReactive:Xo,isReadonly:Yo,isRef:zo,isRuntimeOnly:Jo,isShallow:Zo,isVNode:Qo,markRaw:ko,mergeDefaults:qo,mergeModels:_o,mergeProps:ta,nextTick:ea,nodeOps:ra,normalizeClass:jr,normalizeProps:na,normalizeStyle:dn,onActivated:oa,onBeforeMount:aa,onBeforeUnmount:ia,onBeforeUpdate:sa,onDeactivated:la,onErrorCaptured:ua,onMounted:ca,onRenderTracked:fa,onRenderTriggered:da,onScopeDispose:va,onServerPrefetch:pa,onUnmounted:ha,onUpdated:ga,onWatcherCleanup:ma,openBlock:Q,patchProp:ya,popScopeId:ba,provide:Sa,proxyRefs:xa,pushScopeId:Ea,queuePostFlushCb:Oa,reactive:Mr,readonly:Pa,ref:At,registerRuntimeCompiler:Ca,render:Ta,renderList:fe,renderSlot:Ia,resolveComponent:Ge,resolveDirective:Aa,resolveDynamicComponent:Da,resolveFilter:Ra,resolveTransitionHooks:$a,setBlockTracking:wa,setDevtoolsHook:Fa,setTransitionHooks:ja,shallowReactive:Ma,shallowReadonly:Na,shallowRef:La,ssrContextKey:Ua,ssrUtils:Ba,stop:Ga,toDisplayString:X,toHandlerKey:Va,toHandlers:Ka,toRaw:Ha,toRef:Wa,toRefs:Xa,toValue:Ya,transformVNodeArgs:za,triggerRef:Ja,unref:z,useAttrs:Za,useCssModule:Qa,useCssVars:ka,useHost:qa,useId:_a,useModel:ti,useSSRContext:ei,useShadowRoot:ri,useSlots:ni,useTemplateRef:oi,useTransitionState:ai,vModelCheckbox:ii,vModelDynamic:si,vModelRadio:li,vModelSelect:ui,vModelText:ci,vShow:Pe,version:fi,warn:di,watch:je,watchEffect:vi,watchPostEffect:pi,watchSyncEffect:hi,withAsyncContext:gi,withCtx:tt,withDefaults:mi,withDirectives:Ce,withKeys:yi,withMemo:bi,withModifiers:ir,withScopeId:Si}=cn,xi=Object.freeze(Object.defineProperty({__proto__:null,BaseTransition:jn,BaseTransitionPropsValidators:Mn,Comment:Nn,DeprecationTypes:Ln,EffectScope:Un,ErrorCodes:Bn,ErrorTypeStrings:Gn,Fragment:ce,KeepAlive:Vn,ReactiveEffect:Kn,Static:Hn,Suspense:Wn,Teleport:Xn,Text:Yn,TrackOpTypes:zn,Transition:Jn,TransitionGroup:Zn,TriggerOpTypes:Qn,VueElement:kn,__esModule:qn,assertNumber:_n,callWithAsyncErrorHandling:to,callWithErrorHandling:eo,camelize:ro,capitalize:no,cloneVNode:oo,compatUtils:ao,compile:io,computed:Nt,createApp:so,createBlock:Xt,createCommentVNode:Vt,createElementBlock:st,createElementVNode:B,createHydrationRenderer:lo,createPropsRestProxy:uo,createRenderer:co,createSSRApp:fo,createSlots:vo,createStaticVNode:po,createTextVNode:It,createVNode:J,customRef:ho,default:cn,defineAsyncComponent:Vr,defineComponent:go,defineCustomElement:mo,defineEmits:yo,defineExpose:bo,defineModel:So,defineOptions:xo,defineProps:Eo,defineSSRCustomElement:Oo,defineSlots:Po,devtools:Co,effect:To,effectScope:Io,getCurrentInstance:fn,getCurrentScope:Ao,getCurrentWatcher:Do,getTransitionRawChildren:Ro,guardReactiveProps:$o,h:wo,handleError:Fo,hasInjectionContext:jo,hydrate:Mo,hydrateOnIdle:No,hydrateOnInteraction:Lo,hydrateOnMediaQuery:Uo,hydrateOnVisible:Bo,initCustomFormatter:Go,initDirectivesForSSR:Vo,inject:Ko,isMemoSame:Ho,isProxy:Wo,isReactive:Xo,isReadonly:Yo,isRef:zo,isRuntimeOnly:Jo,isShallow:Zo,isVNode:Qo,markRaw:ko,mergeDefaults:qo,mergeModels:_o,mergeProps:ta,nextTick:ea,nodeOps:ra,normalizeClass:jr,normalizeProps:na,normalizeStyle:dn,onActivated:oa,onBeforeMount:aa,onBeforeUnmount:ia,onBeforeUpdate:sa,onDeactivated:la,onErrorCaptured:ua,onMounted:ca,onRenderTracked:fa,onRenderTriggered:da,onScopeDispose:va,onServerPrefetch:pa,onUnmounted:ha,onUpdated:ga,onWatcherCleanup:ma,openBlock:Q,patchProp:ya,popScopeId:ba,provide:Sa,proxyRefs:xa,pushScopeId:Ea,queuePostFlushCb:Oa,reactive:Mr,readonly:Pa,ref:At,registerRuntimeCompiler:Ca,render:Ta,renderList:fe,renderSlot:Ia,resolveComponent:Ge,resolveDirective:Aa,resolveDynamicComponent:Da,resolveFilter:Ra,resolveTransitionHooks:$a,setBlockTracking:wa,setDevtoolsHook:Fa,setTransitionHooks:ja,shallowReactive:Ma,shallowReadonly:Na,shallowRef:La,ssrContextKey:Ua,ssrUtils:Ba,stop:Ga,toDisplayString:X,toHandlerKey:Va,toHandlers:Ka,toRaw:Ha,toRef:Wa,toRefs:Xa,toValue:Ya,transformVNodeArgs:za,triggerRef:Ja,unref:z,useAttrs:Za,useCssModule:Qa,useCssVars:ka,useHost:qa,useId:_a,useModel:ti,useSSRContext:ei,useShadowRoot:ri,useSlots:ni,useTemplateRef:oi,useTransitionState:ai,vModelCheckbox:ii,vModelDynamic:si,vModelRadio:li,vModelSelect:ui,vModelText:ci,vShow:Pe,version:fi,warn:di,watch:je,watchEffect:vi,watchPostEffect:pi,watchSyncEffect:hi,withAsyncContext:gi,withCtx:tt,withDefaults:mi,withDirectives:Ce,withKeys:yi,withMemo:bi,withModifiers:ir,withScopeId:Si},Symbol.toStringTag,{value:"Module"})),{Fieldtype:te,IndexFieldtype:ru,FieldtypeMixin:nu,HasActionsMixin:ou,HasInputOptionsMixin:au,HasPreferencesMixin:iu,IndexFieldtypeMixin:su,InlineEditForm:lu,DateFormatter:uu,ItemActions:cu,RelatedItem:fu,RestoreRevision:du,RevisionHistory:vu,RevisionPreview:pu,SaveButtonOptions:hu,SortableList:gu,requireElevatedSession:mu,requireElevatedSessionIf:yu,clone:bu,deepClone:Su,resetValuesFromResponse:xu}=__STATAMIC__.core,{Alert:Eu,AuthCard:Ou,Avatar:Pu,Badge:Ei,Button:Ft,ButtonGroup:Cu,Calendar:Tu,Card:Qe,CardList:Iu,CardListItem:Au,CardPanel:Du,CharacterCounter:Ru,Checkbox:$u,CheckboxGroup:wu,CodeEditor:Fu,Combobox:ju,CommandPaletteItem:Mu,ConfirmationModal:xr,Context:Nu,ContextFooter:Lu,ContextHeader:Uu,ContextItem:Bu,ContextLabel:Gu,ContextMenu:Vu,ContextSeparator:Ku,CreateForm:Hu,DatePicker:Wu,DateRangePicker:Xu,Description:Yu,DocsCallout:zu,DragHandle:Oi,Dropdown:Ju,DropdownItem:Zu,DropdownLabel:Qu,DropdownMenu:ku,DropdownSeparator:qu,DropdownFooter:_u,DropdownHeader:tc,Editable:ec,ErrorMessage:rc,EmptyStateItem:nc,EmptyStateMenu:oc,Field:ac,Header:ic,Heading:Er,HoverCard:sc,Icon:zr,Input:Or,InputGroup:lc,InputGroupAppend:uc,InputGroupPrepend:cc,Label:Te,Listing:fc,ListingCustomizeColumns:dc,ListingFilters:vc,ListingHeaderCell:pc,ListingPagination:hc,ListingPresets:gc,ListingPresetTrigger:mc,ListingRowActions:yc,ListingSearch:bc,ListingTable:Sc,ListingTableBody:xc,ListingTableHead:Ec,ListingToggleAll:Oc,LivePreview:Pc,LivePreviewPopout:Cc,MiddleEllipsis:Tc,Modal:Ic,ModalClose:Ac,ModalTitle:Dc,Pagination:Rc,Panel:Jr,PanelFooter:$c,PanelHeader:Zr,Popover:wc,PublishComponents:Fc,PublishContainer:jc,publishContextKey:Mc,injectPublishContext:Pi,PublishField:Nc,PublishFields:Lc,PublishFieldsProvider:Uc,PublishForm:Bc,PublishLocalizations:Gc,PublishSections:Vc,PublishTabs:Kc,Radio:Hc,RadioGroup:Wc,Select:Xc,Separator:Ci,Slider:Yc,Skeleton:zc,SplitterGroup:Jc,SplitterPanel:Zc,SplitterResizeHandle:Qc,StatusIndicator:kc,Subheading:qc,Switch:_c,TabContent:tf,Stack:Ti,StackClose:ef,StackHeader:rf,StackFooter:nf,StackContent:of,Table:af,TableCell:sf,TableColumn:lf,TableColumns:uf,TableRow:cf,TableRows:ff,TabList:df,TabProvider:vf,Tabs:pf,TabTrigger:hf,Textarea:gf,TimePicker:mf,ToggleGroup:yf,ToggleItem:bf,Widget:Sf,registerIconSet:xf,registerIconSetFromStrings:Ef}=__STATAMIC__.ui,vn=(o,e)=>{const n=o.__vccOpts||o;for(const[s,r]of e)n[s]=r;return n},Ii={name:"PresetStack",components:{Badge:Ei,Button:Ft,Card:Qe,Stack:Ti},emits:["close","preset-selected"],props:{visible:{type:Boolean,default:!1},presets:{type:Array,default:()=>[]},hasExistingSchemas:{type:Boolean,default:!1}},data(){return{selectedPreset:null}},computed:{isOpen:{get(){return this.visible},set(o){o||this.close()}}},methods:{close(){this.selectedPreset=null,this.$emit("close")},selectPreset(o){this.selectedPreset=o},goBack(){this.selectedPreset=null},handleAction(o){this.$emit("preset-selected",{preset:this.selectedPreset,action:o}),this.close()}}},Ai={class:"max-h-[60vh] flex flex-col gap-4"},Di={key:0,class:"preset-selection space-y-4"},Ri={class:"text-sm"},$i={class:"grid grid-cols-1 gap-3"},wi={class:"flex justify-between items-start gap-3"},Fi={class:"flex-1"},ji={class:"font-semibold text-sm"},Mi={class:"text-xs mt-1"},Ni={class:"mt-2"},Li={class:"text-[11px] mb-1"},Ui={class:"flex flex-wrap gap-1"},Bi={class:"py-1 px-2"},Gi={class:"py-1 px-2"},Vi={class:"py-1 px-2"},Ki={key:1,class:"preset-actions space-y-4"},Hi={class:"font-semibold"},Wi={class:"text-sm mt-1"},Xi={key:0,class:"action-selection space-y-3"},Yi={class:"text-sm"},zi={class:"flex flex-col gap-1"},Ji={class:"font-semibold"},Zi={class:"text-sm"},Qi={class:"flex flex-col gap-1"},ki={class:"font-semibold"},qi={class:"text-sm"},_i={key:1,class:"no-existing-schemas space-y-3"},ts={class:"text-sm"},es={class:"flex justify-end gap-2"};function rs(o,e,n,s,r,u){const t=Ge("Badge"),a=Ge("Card"),i=Ge("Button"),l=Ge("Stack");return Q(),Xt(l,{open:u.isOpen,"onUpdate:open":e[3]||(e[3]=c=>u.isOpen=c),title:o.__("Add Preset")},{footer:tt(()=>[B("div",es,[r.selectedPreset?(Q(),Xt(i,{key:0,variant:"ghost",size:"sm",onClick:u.goBack},{default:tt(()=>[It(X(o.__("Back")),1)]),_:1},8,["onClick"])):Vt("",!0),J(i,{variant:"subtle",size:"sm",onClick:u.close},{default:tt(()=>[It(X(o.__("Cancel")),1)]),_:1},8,["onClick"])])]),default:tt(()=>[B("div",Ai,[r.selectedPreset?(Q(),st("div",Ki,[J(a,null,{default:tt(()=>[B("h4",Hi,X(r.selectedPreset.name),1),B("p",Wi,X(r.selectedPreset.description),1)]),_:1}),n.hasExistingSchemas?(Q(),st("div",Xi,[B("p",Yi,X(o.__("You have existing schemas. How would you like to add this preset?")),1),J(a,{class:"cursor-pointer",onClick:e[0]||(e[0]=c=>u.handleAction("merge"))},{default:tt(()=>[B("div",zi,[B("div",Ji,X(o.__("Merge (Recommended)")),1),B("div",Zi,X(o.__("Add this preset as an additional schema alongside your existing ones")),1)])]),_:1}),J(a,{class:"cursor-pointer",onClick:e[1]||(e[1]=c=>u.handleAction("override"))},{default:tt(()=>[B("div",Qi,[B("div",ki,X(o.__("Override")),1),B("div",qi,X(o.__("Replace all existing schemas with this preset")),1)])]),_:1})])):(Q(),st("div",_i,[B("p",ts,X(o.__("This preset will be added as your first schema.")),1),J(i,{class:"w-full",onClick:e[2]||(e[2]=c=>u.handleAction("add"))},{default:tt(()=>[It(X(o.__("Add Preset")),1)]),_:1})]))])):(Q(),st("div",Di,[B("p",Ri,X(o.__("Choose a preset to add to your schema:")),1),B("div",$i,[(Q(!0),st(ce,null,fe(n.presets,c=>(Q(),Xt(a,{key:c.name,class:"cursor-pointer",onClick:f=>u.selectPreset(c)},{default:tt(()=>[B("div",wi,[B("div",Fi,[B("h4",ji,X(c.name),1),B("p",Mi,X(c.description),1),B("div",Ni,[B("div",Li,X(o.__("Fields:")),1),B("div",Ui,[(Q(!0),st(ce,null,fe(c.schema.fields.slice(0,3),f=>(Q(),Xt(t,{key:f.key,size:"xs",pill:""},{default:tt(()=>[B("span",Bi,X(f.key),1)]),_:2},1024))),128)),c.schema.fields.length>3?(Q(),Xt(t,{key:0,size:"xs",pill:""},{default:tt(()=>[B("span",Gi," +"+X(c.schema.fields.length-3),1)]),_:2},1024)):Vt("",!0)])])]),J(t,{size:"xs",pill:""},{default:tt(()=>[B("span",Vi,X(c.schema.specialProps.type),1)]),_:2},1024)])]),_:2},1032,["onClick"]))),128))])]))])]),_:1},8,["open","title"])}const ns=vn(Ii,[["render",rs]]);function vr(o){const e={};if(o.specialProps&&(o.specialProps.context&&(e["@context"]=o.specialProps.context),o.specialProps.type&&(e["@type"]=o.specialProps.type),o.specialProps.id&&(e["@id"]=o.specialProps.id)),o.fields&&Array.isArray(o.fields)){for(const n of o.fields)if(n.key)if(n.type==="array"&&n.values)e[n.key]=n.values;else if(n.type==="object"&&n.value)e[n.key]=vr(n.value);else if(n.type==="object_array"&&n.values)e[n.key]=n.values.map(s=>vr(s));else if(n.type==="replicator_object_array"&&n.config){const s=n.config||{};if(s.flat===!0&&s.flat_key_field&&s.flat_value_field){const r=`{{ ${s.flat_key_field} }}`,u=`{{ ${s.flat_value_field} }}`;e[n.key]=[{[r]:u}]}else{const r={};(Array.isArray(s.mappings)?s.mappings:[]).forEach(t=>{t.key&&(t.mode==="static"?r[t.key]=t.static??"":t.mode==="field"?r[t.key]=`{{ ${t.field||"field"} }}`:t.mode==="nested_replicator"?r[t.key]=[{}]:r[t.key]="")}),e[n.key]=[r]}}else e[n.key]=n.value??null}return e}function pn(o,e=2){try{if(Array.isArray(o)){const s=o.map(r=>vr(r));return JSON.stringify(s,null,e)}const n=vr(o);return JSON.stringify(n,null,e)}catch{return JSON.stringify(o,null,e)}}const os=/[^a-zA-Z0-9@:-]/g,as=o=>typeof o!="string"?"":o.replace(os,""),hn=o=>{!o||typeof o!="object"||(o.key=as(o.key))},gn=(o,{objectArrayType:e}={})=>{if(!(!o||typeof o!="object")){if(Array.isArray(o.values)||(o.values=[]),e&&o.type===e){o.values.push({specialProps:{type:"",id:""},fields:[]});return}o.values.push("")}},mn=(o,e)=>{!o||typeof o!="object"||!Array.isArray(o.values)||o.values.splice(e,1)},yn=(o,{objectType:e="object",arrayType:n="array",objectArrayType:s,dataObjectType:r,replicatorObjectArrayType:u="replicator_object_array"}={})=>{if(!(!o||typeof o!="object")){if(o.type===e){o.value={specialProps:{type:"",id:""},fields:[]};return}if(o.type===n){o.values=[];return}if(s&&o.type===s){o.values=[];return}if(r&&o.type===r){o.value="";return}if(o.type===u){o.config={replicator_field:"",set:"",mappings:[]},o.values=[];return}o.value=""}};function is(o){return o&&o.__esModule&&Object.prototype.hasOwnProperty.call(o,"default")?o.default:o}function bn(o){if(Object.prototype.hasOwnProperty.call(o,"__esModule"))return o;var e=o.default;if(typeof e=="function"){var n=function s(){return this instanceof s?Reflect.construct(e,arguments,this.constructor):e.apply(this,arguments)};n.prototype=e.prototype}else n={};return Object.defineProperty(n,"__esModule",{value:!0}),Object.keys(o).forEach(function(s){var r=Object.getOwnPropertyDescriptor(o,s);Object.defineProperty(n,s,r.get?r:{enumerable:!0,get:function(){return o[s]}})}),n}var sr={exports:{}};const ss=bn(xi);/**! +const Fn="modulepreload",jn=function(o){return"/build/"+o},Yr={},un=function(e,n,s){let r=Promise.resolve();if(n&&n.length>0){let t=function(l){return Promise.all(l.map(c=>Promise.resolve(c).then(f=>({status:"fulfilled",value:f}),f=>({status:"rejected",reason:f}))))};document.getElementsByTagName("link");const a=document.querySelector("meta[property=csp-nonce]"),i=(a==null?void 0:a.nonce)||(a==null?void 0:a.getAttribute("nonce"));r=t(n.map(l=>{if(l=jn(l),l in Yr)return;Yr[l]=!0;const c=l.endsWith(".css"),f=c?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${l}"]${f}`))return;const d=document.createElement("link");if(d.rel=c?"stylesheet":Fn,c||(d.as="script"),d.crossOrigin="",d.href=l,i&&d.setAttribute("nonce",i),document.head.appendChild(d),c)return new Promise((v,p)=>{d.addEventListener("load",v),d.addEventListener("error",()=>p(new Error(`Unable to preload CSS for ${l}`)))})}))}function u(t){const a=new Event("vite:preloadError",{cancelable:!0});if(a.payload=t,window.dispatchEvent(a),!a.defaultPrevented)throw t}return r.then(t=>{for(const a of t||[])a.status==="rejected"&&u(a.reason);return e().catch(u)})},cn=window.Vue,{BaseTransition:Mn,BaseTransitionPropsValidators:Nn,Comment:Ln,DeprecationTypes:$n,EffectScope:Un,ErrorCodes:Bn,ErrorTypeStrings:Gn,Fragment:ce,KeepAlive:Vn,ReactiveEffect:Kn,Static:Hn,Suspense:Wn,Teleport:Xn,Text:Yn,TrackOpTypes:zn,Transition:Jn,TransitionGroup:Zn,TriggerOpTypes:Qn,VueElement:kn,__esModule:qn,assertNumber:_n,callWithAsyncErrorHandling:to,callWithErrorHandling:eo,camelize:ro,capitalize:no,cloneVNode:oo,compatUtils:ao,compile:io,computed:Lt,createApp:so,createBlock:Xt,createCommentVNode:Vt,createElementBlock:st,createElementVNode:B,createHydrationRenderer:lo,createPropsRestProxy:uo,createRenderer:co,createSSRApp:fo,createSlots:vo,createStaticVNode:po,createTextVNode:It,createVNode:J,customRef:ho,defineAsyncComponent:Vr,defineComponent:go,defineCustomElement:mo,defineEmits:yo,defineExpose:bo,defineModel:So,defineOptions:xo,defineProps:Eo,defineSSRCustomElement:Oo,defineSlots:Po,devtools:Co,effect:To,effectScope:Io,getCurrentInstance:fn,getCurrentScope:Ao,getCurrentWatcher:Do,getTransitionRawChildren:Ro,guardReactiveProps:wo,h:Fo,handleError:jo,hasInjectionContext:Mo,hydrate:No,hydrateOnIdle:Lo,hydrateOnInteraction:$o,hydrateOnMediaQuery:Uo,hydrateOnVisible:Bo,initCustomFormatter:Go,initDirectivesForSSR:Vo,inject:Ko,isMemoSame:Ho,isProxy:Wo,isReactive:Xo,isReadonly:Yo,isRef:zo,isRuntimeOnly:Jo,isShallow:Zo,isVNode:Qo,markRaw:ko,mergeDefaults:qo,mergeModels:_o,mergeProps:ta,nextTick:ea,nodeOps:ra,normalizeClass:Mr,normalizeProps:na,normalizeStyle:dn,onActivated:oa,onBeforeMount:aa,onBeforeUnmount:ia,onBeforeUpdate:sa,onDeactivated:la,onErrorCaptured:ua,onMounted:ca,onRenderTracked:fa,onRenderTriggered:da,onScopeDispose:va,onServerPrefetch:pa,onUnmounted:ha,onUpdated:ga,onWatcherCleanup:ma,openBlock:Q,patchProp:ya,popScopeId:ba,provide:Sa,proxyRefs:xa,pushScopeId:Ea,queuePostFlushCb:Oa,reactive:Nr,readonly:Pa,ref:At,registerRuntimeCompiler:Ca,render:Ta,renderList:fe,renderSlot:Ia,resolveComponent:Ge,resolveDirective:Aa,resolveDynamicComponent:Da,resolveFilter:Ra,resolveTransitionHooks:wa,setBlockTracking:Fa,setDevtoolsHook:ja,setTransitionHooks:Ma,shallowReactive:Na,shallowReadonly:La,shallowRef:$a,ssrContextKey:Ua,ssrUtils:Ba,stop:Ga,toDisplayString:X,toHandlerKey:Va,toHandlers:Ka,toRaw:Ha,toRef:Wa,toRefs:Xa,toValue:Ya,transformVNodeArgs:za,triggerRef:Ja,unref:z,useAttrs:Za,useCssModule:Qa,useCssVars:ka,useHost:qa,useId:_a,useModel:ti,useSSRContext:ei,useShadowRoot:ri,useSlots:ni,useTemplateRef:oi,useTransitionState:ai,vModelCheckbox:ii,vModelDynamic:si,vModelRadio:li,vModelSelect:ui,vModelText:ci,vShow:Pe,version:fi,warn:di,watch:Me,watchEffect:vi,watchPostEffect:pi,watchSyncEffect:hi,withAsyncContext:gi,withCtx:tt,withDefaults:mi,withDirectives:Ce,withKeys:yi,withMemo:bi,withModifiers:ir,withScopeId:Si}=cn,xi=Object.freeze(Object.defineProperty({__proto__:null,BaseTransition:Mn,BaseTransitionPropsValidators:Nn,Comment:Ln,DeprecationTypes:$n,EffectScope:Un,ErrorCodes:Bn,ErrorTypeStrings:Gn,Fragment:ce,KeepAlive:Vn,ReactiveEffect:Kn,Static:Hn,Suspense:Wn,Teleport:Xn,Text:Yn,TrackOpTypes:zn,Transition:Jn,TransitionGroup:Zn,TriggerOpTypes:Qn,VueElement:kn,__esModule:qn,assertNumber:_n,callWithAsyncErrorHandling:to,callWithErrorHandling:eo,camelize:ro,capitalize:no,cloneVNode:oo,compatUtils:ao,compile:io,computed:Lt,createApp:so,createBlock:Xt,createCommentVNode:Vt,createElementBlock:st,createElementVNode:B,createHydrationRenderer:lo,createPropsRestProxy:uo,createRenderer:co,createSSRApp:fo,createSlots:vo,createStaticVNode:po,createTextVNode:It,createVNode:J,customRef:ho,default:cn,defineAsyncComponent:Vr,defineComponent:go,defineCustomElement:mo,defineEmits:yo,defineExpose:bo,defineModel:So,defineOptions:xo,defineProps:Eo,defineSSRCustomElement:Oo,defineSlots:Po,devtools:Co,effect:To,effectScope:Io,getCurrentInstance:fn,getCurrentScope:Ao,getCurrentWatcher:Do,getTransitionRawChildren:Ro,guardReactiveProps:wo,h:Fo,handleError:jo,hasInjectionContext:Mo,hydrate:No,hydrateOnIdle:Lo,hydrateOnInteraction:$o,hydrateOnMediaQuery:Uo,hydrateOnVisible:Bo,initCustomFormatter:Go,initDirectivesForSSR:Vo,inject:Ko,isMemoSame:Ho,isProxy:Wo,isReactive:Xo,isReadonly:Yo,isRef:zo,isRuntimeOnly:Jo,isShallow:Zo,isVNode:Qo,markRaw:ko,mergeDefaults:qo,mergeModels:_o,mergeProps:ta,nextTick:ea,nodeOps:ra,normalizeClass:Mr,normalizeProps:na,normalizeStyle:dn,onActivated:oa,onBeforeMount:aa,onBeforeUnmount:ia,onBeforeUpdate:sa,onDeactivated:la,onErrorCaptured:ua,onMounted:ca,onRenderTracked:fa,onRenderTriggered:da,onScopeDispose:va,onServerPrefetch:pa,onUnmounted:ha,onUpdated:ga,onWatcherCleanup:ma,openBlock:Q,patchProp:ya,popScopeId:ba,provide:Sa,proxyRefs:xa,pushScopeId:Ea,queuePostFlushCb:Oa,reactive:Nr,readonly:Pa,ref:At,registerRuntimeCompiler:Ca,render:Ta,renderList:fe,renderSlot:Ia,resolveComponent:Ge,resolveDirective:Aa,resolveDynamicComponent:Da,resolveFilter:Ra,resolveTransitionHooks:wa,setBlockTracking:Fa,setDevtoolsHook:ja,setTransitionHooks:Ma,shallowReactive:Na,shallowReadonly:La,shallowRef:$a,ssrContextKey:Ua,ssrUtils:Ba,stop:Ga,toDisplayString:X,toHandlerKey:Va,toHandlers:Ka,toRaw:Ha,toRef:Wa,toRefs:Xa,toValue:Ya,transformVNodeArgs:za,triggerRef:Ja,unref:z,useAttrs:Za,useCssModule:Qa,useCssVars:ka,useHost:qa,useId:_a,useModel:ti,useSSRContext:ei,useShadowRoot:ri,useSlots:ni,useTemplateRef:oi,useTransitionState:ai,vModelCheckbox:ii,vModelDynamic:si,vModelRadio:li,vModelSelect:ui,vModelText:ci,vShow:Pe,version:fi,warn:di,watch:Me,watchEffect:vi,watchPostEffect:pi,watchSyncEffect:hi,withAsyncContext:gi,withCtx:tt,withDefaults:mi,withDirectives:Ce,withKeys:yi,withMemo:bi,withModifiers:ir,withScopeId:Si},Symbol.toStringTag,{value:"Module"})),{Fieldtype:te,IndexFieldtype:ru,FieldtypeMixin:nu,HasActionsMixin:ou,HasInputOptionsMixin:au,HasPreferencesMixin:iu,IndexFieldtypeMixin:su,InlineEditForm:lu,DateFormatter:uu,NumberFormatter:cu,ItemActions:fu,RelatedItem:du,RestoreRevision:vu,RevisionHistory:pu,RevisionPreview:hu,SaveButtonOptions:gu,SortableList:mu,requireElevatedSession:yu,requireElevatedSessionIf:bu,clone:Su,debounce:xu,deepClone:Eu,resetValuesFromResponse:Ou}=__STATAMIC__.core,{Alert:Pu,AuthCard:Cu,Avatar:Tu,Badge:Ei,Button:jt,ButtonGroup:Iu,Calendar:Au,Card:Qe,CardList:Du,CardListItem:Ru,CardPanel:wu,CharacterCounter:Fu,Checkbox:ju,CheckboxGroup:Mu,CodeEditor:Nu,Combobox:Lu,CommandPaletteItem:$u,ConfirmationModal:xr,Context:Uu,ContextFooter:Bu,ContextHeader:Gu,ContextItem:Vu,ContextLabel:Ku,ContextMenu:Hu,ContextSeparator:Wu,CreateForm:Xu,DatePicker:Yu,DateRangePicker:zu,Description:Ju,DocsCallout:Zu,DragHandle:Oi,Dropdown:Qu,DropdownItem:ku,DropdownLabel:qu,DropdownMenu:_u,DropdownSeparator:tc,DropdownFooter:ec,DropdownHeader:rc,Editable:nc,ErrorMessage:oc,EmptyStateItem:ac,EmptyStateMenu:ic,Field:sc,Header:lc,Heading:Er,HoverCard:uc,Icon:zr,Input:Or,InputGroup:cc,InputGroupAppend:fc,InputGroupPrepend:dc,Label:Te,Listing:vc,ListingCustomizeColumns:pc,ListingFilters:hc,ListingHeaderCell:gc,ListingPagination:mc,ListingPresets:yc,ListingPresetTrigger:bc,ListingRowActions:Sc,ListingSearch:xc,ListingTable:Ec,ListingTableBody:Oc,ListingTableHead:Pc,ListingToggleAll:Cc,LivePreview:Tc,LivePreviewPopout:Ic,MiddleEllipsis:Ac,Modal:Dc,ModalClose:Rc,ModalTitle:wc,Pagination:Fc,Panel:Jr,PanelFooter:jc,PanelHeader:Zr,Popover:Mc,PublishComponents:Nc,PublishContainer:Lc,publishContextKey:$c,injectPublishContext:Pi,PublishField:Uc,PublishFields:Bc,PublishFieldsProvider:Gc,PublishForm:Vc,PublishLocalizations:Kc,PublishSections:Hc,PublishTabs:Wc,Radio:Xc,RadioGroup:Yc,Select:zc,Separator:Ci,Slider:Jc,Skeleton:Zc,SplitterGroup:Qc,SplitterPanel:kc,SplitterResizeHandle:qc,StatusIndicator:_c,Subheading:tf,Switch:ef,TabContent:rf,Stack:Ti,StackClose:nf,StackHeader:of,StackFooter:af,StackContent:sf,Table:lf,TableCell:uf,TableColumn:cf,TableColumns:ff,TableRow:df,TableRows:vf,TabList:pf,TabProvider:hf,Tabs:gf,TabTrigger:mf,Text:yf,Textarea:bf,TimePicker:Sf,TimezoneHoverCard:xf,Timezones:Ef,ToggleGroup:Of,ToggleItem:Pf,Widget:Cf,registerIconSet:Tf,registerIconSetFromStrings:If}=__STATAMIC__.ui,vn=(o,e)=>{const n=o.__vccOpts||o;for(const[s,r]of e)n[s]=r;return n},Ii={name:"PresetStack",components:{Badge:Ei,Button:jt,Card:Qe,Stack:Ti},emits:["close","preset-selected"],props:{visible:{type:Boolean,default:!1},presets:{type:Array,default:()=>[]},hasExistingSchemas:{type:Boolean,default:!1}},data(){return{selectedPreset:null}},computed:{isOpen:{get(){return this.visible},set(o){o||this.close()}}},methods:{close(){this.selectedPreset=null,this.$emit("close")},selectPreset(o){this.selectedPreset=o},goBack(){this.selectedPreset=null},handleAction(o){this.$emit("preset-selected",{preset:this.selectedPreset,action:o}),this.close()}}},Ai={class:"max-h-[60vh] flex flex-col gap-4"},Di={key:0,class:"preset-selection space-y-4"},Ri={class:"text-sm"},wi={class:"grid grid-cols-1 gap-3"},Fi={class:"flex justify-between items-start gap-3"},ji={class:"flex-1"},Mi={class:"font-semibold text-sm"},Ni={class:"text-xs mt-1"},Li={class:"mt-2"},$i={class:"text-[11px] mb-1"},Ui={class:"flex flex-wrap gap-1"},Bi={class:"py-1 px-2"},Gi={class:"py-1 px-2"},Vi={class:"py-1 px-2"},Ki={key:1,class:"preset-actions space-y-4"},Hi={class:"font-semibold"},Wi={class:"text-sm mt-1"},Xi={key:0,class:"action-selection space-y-3"},Yi={class:"text-sm"},zi={class:"flex flex-col gap-1"},Ji={class:"font-semibold"},Zi={class:"text-sm"},Qi={class:"flex flex-col gap-1"},ki={class:"font-semibold"},qi={class:"text-sm"},_i={key:1,class:"no-existing-schemas space-y-3"},ts={class:"text-sm"},es={class:"flex justify-end gap-2"};function rs(o,e,n,s,r,u){const t=Ge("Badge"),a=Ge("Card"),i=Ge("Button"),l=Ge("Stack");return Q(),Xt(l,{open:u.isOpen,"onUpdate:open":e[3]||(e[3]=c=>u.isOpen=c),title:o.__("Add Preset")},{footer:tt(()=>[B("div",es,[r.selectedPreset?(Q(),Xt(i,{key:0,variant:"ghost",size:"sm",onClick:u.goBack},{default:tt(()=>[It(X(o.__("Back")),1)]),_:1},8,["onClick"])):Vt("",!0),J(i,{variant:"subtle",size:"sm",onClick:u.close},{default:tt(()=>[It(X(o.__("Cancel")),1)]),_:1},8,["onClick"])])]),default:tt(()=>[B("div",Ai,[r.selectedPreset?(Q(),st("div",Ki,[J(a,null,{default:tt(()=>[B("h4",Hi,X(r.selectedPreset.name),1),B("p",Wi,X(r.selectedPreset.description),1)]),_:1}),n.hasExistingSchemas?(Q(),st("div",Xi,[B("p",Yi,X(o.__("You have existing schemas. How would you like to add this preset?")),1),J(a,{class:"cursor-pointer",onClick:e[0]||(e[0]=c=>u.handleAction("merge"))},{default:tt(()=>[B("div",zi,[B("div",Ji,X(o.__("Merge (Recommended)")),1),B("div",Zi,X(o.__("Add this preset as an additional schema alongside your existing ones")),1)])]),_:1}),J(a,{class:"cursor-pointer",onClick:e[1]||(e[1]=c=>u.handleAction("override"))},{default:tt(()=>[B("div",Qi,[B("div",ki,X(o.__("Override")),1),B("div",qi,X(o.__("Replace all existing schemas with this preset")),1)])]),_:1})])):(Q(),st("div",_i,[B("p",ts,X(o.__("This preset will be added as your first schema.")),1),J(i,{class:"w-full",onClick:e[2]||(e[2]=c=>u.handleAction("add"))},{default:tt(()=>[It(X(o.__("Add Preset")),1)]),_:1})]))])):(Q(),st("div",Di,[B("p",Ri,X(o.__("Choose a preset to add to your schema:")),1),B("div",wi,[(Q(!0),st(ce,null,fe(n.presets,c=>(Q(),Xt(a,{key:c.name,class:"cursor-pointer",onClick:f=>u.selectPreset(c)},{default:tt(()=>[B("div",Fi,[B("div",ji,[B("h4",Mi,X(c.name),1),B("p",Ni,X(c.description),1),B("div",Li,[B("div",$i,X(o.__("Fields:")),1),B("div",Ui,[(Q(!0),st(ce,null,fe(c.schema.fields.slice(0,3),f=>(Q(),Xt(t,{key:f.key,size:"xs",pill:""},{default:tt(()=>[B("span",Bi,X(f.key),1)]),_:2},1024))),128)),c.schema.fields.length>3?(Q(),Xt(t,{key:0,size:"xs",pill:""},{default:tt(()=>[B("span",Gi," +"+X(c.schema.fields.length-3),1)]),_:2},1024)):Vt("",!0)])])]),J(t,{size:"xs",pill:""},{default:tt(()=>[B("span",Vi,X(c.schema.specialProps.type),1)]),_:2},1024)])]),_:2},1032,["onClick"]))),128))])]))])]),_:1},8,["open","title"])}const ns=vn(Ii,[["render",rs]]);function vr(o){const e={};if(o.specialProps&&(o.specialProps.context&&(e["@context"]=o.specialProps.context),o.specialProps.type&&(e["@type"]=o.specialProps.type),o.specialProps.id&&(e["@id"]=o.specialProps.id)),o.fields&&Array.isArray(o.fields)){for(const n of o.fields)if(n.key)if(n.type==="array"&&n.values)e[n.key]=n.values;else if(n.type==="object"&&n.value)e[n.key]=vr(n.value);else if(n.type==="object_array"&&n.values)e[n.key]=n.values.map(s=>vr(s));else if(n.type==="replicator_object_array"&&n.config){const s=n.config||{};if(s.flat===!0&&s.flat_key_field&&s.flat_value_field){const r=`{{ ${s.flat_key_field} }}`,u=`{{ ${s.flat_value_field} }}`;e[n.key]=[{[r]:u}]}else{const r={};(Array.isArray(s.mappings)?s.mappings:[]).forEach(t=>{t.key&&(t.mode==="static"?r[t.key]=t.static??"":t.mode==="field"?r[t.key]=`{{ ${t.field||"field"} }}`:t.mode==="nested_replicator"?r[t.key]=[{}]:r[t.key]="")}),e[n.key]=[r]}}else e[n.key]=n.value??null}return e}function pn(o,e=2){try{if(Array.isArray(o)){const s=o.map(r=>vr(r));return JSON.stringify(s,null,e)}const n=vr(o);return JSON.stringify(n,null,e)}catch{return JSON.stringify(o,null,e)}}const os=/[^a-zA-Z0-9@:-]/g,as=o=>typeof o!="string"?"":o.replace(os,""),hn=o=>{!o||typeof o!="object"||(o.key=as(o.key))},gn=(o,{objectArrayType:e}={})=>{if(!(!o||typeof o!="object")){if(Array.isArray(o.values)||(o.values=[]),e&&o.type===e){o.values.push({specialProps:{type:"",id:""},fields:[]});return}o.values.push("")}},mn=(o,e)=>{!o||typeof o!="object"||!Array.isArray(o.values)||o.values.splice(e,1)},yn=(o,{objectType:e="object",arrayType:n="array",objectArrayType:s,dataObjectType:r,replicatorObjectArrayType:u="replicator_object_array"}={})=>{if(!(!o||typeof o!="object")){if(o.type===e){o.value={specialProps:{type:"",id:""},fields:[]};return}if(o.type===n){o.values=[];return}if(s&&o.type===s){o.values=[];return}if(r&&o.type===r){o.value="";return}if(o.type===u){o.config={replicator_field:"",set:"",mappings:[]},o.values=[];return}o.value=""}};function is(o){return o&&o.__esModule&&Object.prototype.hasOwnProperty.call(o,"default")?o.default:o}function bn(o){if(Object.prototype.hasOwnProperty.call(o,"__esModule"))return o;var e=o.default;if(typeof e=="function"){var n=function s(){return this instanceof s?Reflect.construct(e,arguments,this.constructor):e.apply(this,arguments)};n.prototype=e.prototype}else n={};return Object.defineProperty(n,"__esModule",{value:!0}),Object.keys(o).forEach(function(s){var r=Object.getOwnPropertyDescriptor(o,s);Object.defineProperty(n,s,r.get?r:{enumerable:!0,get:function(){return o[s]}})}),n}var sr={exports:{}};const ss=bn(xi);/**! * Sortable 1.14.0 * @author RubaXa * @author owenm * @license MIT - */function Qr(o,e){var n=Object.keys(o);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(o);e&&(s=s.filter(function(r){return Object.getOwnPropertyDescriptor(o,r).enumerable})),n.push.apply(n,s)}return n}function le(o){for(var e=1;e=0)&&(n[r]=o[r]);return n}function cs(o,e){if(o==null)return{};var n=us(o,e),s,r;if(Object.getOwnPropertySymbols){var u=Object.getOwnPropertySymbols(o);for(r=0;r=0)&&Object.prototype.propertyIsEnumerable.call(o,s)&&(n[s]=o[s])}return n}function fs(o){return ds(o)||vs(o)||ps(o)||hs()}function ds(o){if(Array.isArray(o))return Nr(o)}function vs(o){if(typeof Symbol<"u"&&o[Symbol.iterator]!=null||o["@@iterator"]!=null)return Array.from(o)}function ps(o,e){if(o){if(typeof o=="string")return Nr(o,e);var n=Object.prototype.toString.call(o).slice(8,-1);if(n==="Object"&&o.constructor&&(n=o.constructor.name),n==="Map"||n==="Set")return Array.from(o);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return Nr(o,e)}}function Nr(o,e){(e==null||e>o.length)&&(e=o.length);for(var n=0,s=new Array(e);n"&&(e=e.substring(1)),o)try{if(o.matches)return o.matches(e);if(o.msMatchesSelector)return o.msMatchesSelector(e);if(o.webkitMatchesSelector)return o.webkitMatchesSelector(e)}catch{return!1}return!1}}function ys(o){return o.host&&o!==document&&o.host.nodeType?o.host:o.parentNode}function ae(o,e,n,s){if(o){n=n||document;do{if(e!=null&&(e[0]===">"?o.parentNode===n&&pr(o,e):pr(o,e))||s&&o===n)return o;if(o===n)break}while(o=ys(o))}return null}var qr=/\s+/g;function bt(o,e,n){if(o&&e)if(o.classList)o.classList[n?"add":"remove"](e);else{var s=(" "+o.className+" ").replace(qr," ").replace(" "+e+" "," ");o.className=(s+(n?" "+e:"")).replace(qr," ")}}function V(o,e,n){var s=o&&o.style;if(s){if(n===void 0)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(o,""):o.currentStyle&&(n=o.currentStyle),e===void 0?n:n[e];!(e in s)&&e.indexOf("webkit")===-1&&(e="-webkit-"+e),s[e]=n+(typeof n=="string"?"":"px")}}function Ie(o,e){var n="";if(typeof o=="string")n=o;else do{var s=V(o,"transform");s&&s!=="none"&&(n=s+" "+n)}while(!e&&(o=o.parentNode));var r=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return r&&new r(n)}function En(o,e,n){if(o){var s=o.getElementsByTagName(e),r=0,u=s.length;if(n)for(;r=u,!t)return s;if(s===se())break;s=me(s,!1)}return!1}function Me(o,e,n,s){for(var r=0,u=0,t=o.children;u2&&arguments[2]!==void 0?arguments[2]:{},r=s.evt,u=cs(s,Cs);qe.pluginEvent.bind(Z)(e,n,le({dragEl:w,parentEl:Et,ghostEl:rt,rootEl:mt,nextEl:Oe,lastDownEl:ur,cloneEl:Ot,cloneHidden:ge,dragStarted:Ke,putSortable:Mt,activeSortable:Z.active,originalEvent:r,oldIndex:Fe,oldDraggableIndex:ze,newIndex:Zt,newDraggableIndex:he,hideGhostForTarget:An,unhideGhostForTarget:Dn,cloneNowHidden:function(){ge=!0},cloneNowShown:function(){ge=!1},dispatchSortableEvent:function(a){Gt({sortable:n,name:a,originalEvent:r})}},u))};function Gt(o){Ve(le({putSortable:Mt,cloneEl:Ot,targetEl:w,rootEl:mt,oldIndex:Fe,oldDraggableIndex:ze,newIndex:Zt,newDraggableIndex:he},o))}var w,Et,rt,mt,Oe,ur,Ot,ge,Fe,Zt,ze,he,tr,Mt,we=!1,hr=!1,gr=[],xe,ne,Ir,Ar,rn,nn,Ke,Re,Je,Ze=!1,er=!1,cr,Lt,Dr=[],Lr=!1,mr=[],br=typeof document<"u",rr=Sn,on=ke||ve?"cssFloat":"float",Ts=br&&!ms&&!Sn&&"draggable"in document.createElement("div"),Cn=(function(){if(br){if(ve)return!1;var o=document.createElement("x");return o.style.cssText="pointer-events:auto",o.style.pointerEvents==="auto"}})(),Tn=function(e,n){var s=V(e),r=parseInt(s.width)-parseInt(s.paddingLeft)-parseInt(s.paddingRight)-parseInt(s.borderLeftWidth)-parseInt(s.borderRightWidth),u=Me(e,0,n),t=Me(e,1,n),a=u&&V(u),i=t&&V(t),l=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+yt(u).width,c=i&&parseInt(i.marginLeft)+parseInt(i.marginRight)+yt(t).width;if(s.display==="flex")return s.flexDirection==="column"||s.flexDirection==="column-reverse"?"vertical":"horizontal";if(s.display==="grid")return s.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(u&&a.float&&a.float!=="none"){var f=a.float==="left"?"left":"right";return t&&(i.clear==="both"||i.clear===f)?"vertical":"horizontal"}return u&&(a.display==="block"||a.display==="flex"||a.display==="table"||a.display==="grid"||l>=r&&s[on]==="none"||t&&s[on]==="none"&&l+c>r)?"vertical":"horizontal"},Is=function(e,n,s){var r=s?e.left:e.top,u=s?e.right:e.bottom,t=s?e.width:e.height,a=s?n.left:n.top,i=s?n.right:n.bottom,l=s?n.width:n.height;return r===a||u===i||r+t/2===a+l/2},As=function(e,n){var s;return gr.some(function(r){var u=r[Ut].options.emptyInsertThreshold;if(!(!u||Kr(r))){var t=yt(r),a=e>=t.left-u&&e<=t.right+u,i=n>=t.top-u&&n<=t.bottom+u;if(a&&i)return s=r}}),s},In=function(e){function n(u,t){return function(a,i,l,c){var f=a.options.group.name&&i.options.group.name&&a.options.group.name===i.options.group.name;if(u==null&&(t||f))return!0;if(u==null||u===!1)return!1;if(t&&u==="clone")return u;if(typeof u=="function")return n(u(a,i,l,c),t)(a,i,l,c);var d=(t?a:i).options.group.name;return u===!0||typeof u=="string"&&u===d||u.join&&u.indexOf(d)>-1}}var s={},r=e.group;(!r||lr(r)!="object")&&(r={name:r}),s.name=r.name,s.checkPull=n(r.pull,!0),s.checkPut=n(r.put),s.revertClone=r.revertClone,e.group=s},An=function(){!Cn&&rt&&V(rt,"display","none")},Dn=function(){!Cn&&rt&&V(rt,"display","")};br&&document.addEventListener("click",function(o){if(hr)return o.preventDefault(),o.stopPropagation&&o.stopPropagation(),o.stopImmediatePropagation&&o.stopImmediatePropagation(),hr=!1,!1},!0);var Ee=function(e){if(w){e=e.touches?e.touches[0]:e;var n=As(e.clientX,e.clientY);if(n){var s={};for(var r in e)e.hasOwnProperty(r)&&(s[r]=e[r]);s.target=s.rootEl=n,s.preventDefault=void 0,s.stopPropagation=void 0,n[Ut]._onDragOver(s)}}},Ds=function(e){w&&w.parentNode[Ut]._isOutsideThisEl(e.target)};function Z(o,e){if(!(o&&o.nodeType&&o.nodeType===1))throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(o));this.el=o,this.options=e=ee({},e),o[Ut]=this;var n={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(o.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Tn(o,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,a){t.setData("Text",a.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:Z.supportPointer!==!1&&"PointerEvent"in window&&!Xe,emptyInsertThreshold:5};qe.initializePlugins(this,o,n);for(var s in n)!(s in e)&&(e[s]=n[s]);In(e);for(var r in this)r.charAt(0)==="_"&&typeof this[r]=="function"&&(this[r]=this[r].bind(this));this.nativeDraggable=e.forceFallback?!1:Ts,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?at(o,"pointerdown",this._onTapStart):(at(o,"mousedown",this._onTapStart),at(o,"touchstart",this._onTapStart)),this.nativeDraggable&&(at(o,"dragover",this),at(o,"dragenter",this)),gr.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),ee(this,Es())}Z.prototype={constructor:Z,_isOutsideThisEl:function(e){!this.el.contains(e)&&e!==this.el&&(Re=null)},_getDirection:function(e,n){return typeof this.options.direction=="function"?this.options.direction.call(this,e,n,w):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,s=this.el,r=this.options,u=r.preventOnFilter,t=e.type,a=e.touches&&e.touches[0]||e.pointerType&&e.pointerType==="touch"&&e,i=(a||e).target,l=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||i,c=r.filter;if(Ls(s),!w&&!(/mousedown|pointerdown/.test(t)&&e.button!==0||r.disabled)&&!l.isContentEditable&&!(!this.nativeDraggable&&Xe&&i&&i.tagName.toUpperCase()==="SELECT")&&(i=ae(i,r.draggable,s,!1),!(i&&i.animated)&&ur!==i)){if(Fe=Pt(i),ze=Pt(i,r.draggable),typeof c=="function"){if(c.call(this,e,i,this)){Gt({sortable:n,rootEl:l,name:"filter",targetEl:i,toEl:s,fromEl:s}),Ht("filter",n,{evt:e}),u&&e.cancelable&&e.preventDefault();return}}else if(c&&(c=c.split(",").some(function(f){if(f=ae(l,f.trim(),s,!1),f)return Gt({sortable:n,rootEl:f,name:"filter",targetEl:i,fromEl:s,toEl:s}),Ht("filter",n,{evt:e}),!0}),c)){u&&e.cancelable&&e.preventDefault();return}r.handle&&!ae(l,r.handle,s,!1)||this._prepareDragStart(e,a,i)}}},_prepareDragStart:function(e,n,s){var r=this,u=r.el,t=r.options,a=u.ownerDocument,i;if(s&&!w&&s.parentNode===u){var l=yt(s);if(mt=u,w=s,Et=w.parentNode,Oe=w.nextSibling,ur=s,tr=t.group,Z.dragged=w,xe={target:w,clientX:(n||e).clientX,clientY:(n||e).clientY},rn=xe.clientX-l.left,nn=xe.clientY-l.top,this._lastX=(n||e).clientX,this._lastY=(n||e).clientY,w.style["will-change"]="all",i=function(){if(Ht("delayEnded",r,{evt:e}),Z.eventCanceled){r._onDrop();return}r._disableDelayedDragEvents(),!kr&&r.nativeDraggable&&(w.draggable=!0),r._triggerDragStart(e,n),Gt({sortable:r,name:"choose",originalEvent:e}),bt(w,t.chosenClass,!0)},t.ignore.split(",").forEach(function(c){En(w,c.trim(),Rr)}),at(a,"dragover",Ee),at(a,"mousemove",Ee),at(a,"touchmove",Ee),at(a,"mouseup",r._onDrop),at(a,"touchend",r._onDrop),at(a,"touchcancel",r._onDrop),kr&&this.nativeDraggable&&(this.options.touchStartThreshold=4,w.draggable=!0),Ht("delayStart",this,{evt:e}),t.delay&&(!t.delayOnTouchOnly||n)&&(!this.nativeDraggable||!(ke||ve))){if(Z.eventCanceled){this._onDrop();return}at(a,"mouseup",r._disableDelayedDrag),at(a,"touchend",r._disableDelayedDrag),at(a,"touchcancel",r._disableDelayedDrag),at(a,"mousemove",r._delayedDragTouchMoveHandler),at(a,"touchmove",r._delayedDragTouchMoveHandler),t.supportPointer&&at(a,"pointermove",r._delayedDragTouchMoveHandler),r._dragStartTimer=setTimeout(i,t.delay)}else i()}},_delayedDragTouchMoveHandler:function(e){var n=e.touches?e.touches[0]:e;Math.max(Math.abs(n.clientX-this._lastX),Math.abs(n.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){w&&Rr(w),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var e=this.el.ownerDocument;ot(e,"mouseup",this._disableDelayedDrag),ot(e,"touchend",this._disableDelayedDrag),ot(e,"touchcancel",this._disableDelayedDrag),ot(e,"mousemove",this._delayedDragTouchMoveHandler),ot(e,"touchmove",this._delayedDragTouchMoveHandler),ot(e,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(e,n){n=n||e.pointerType=="touch"&&e,!this.nativeDraggable||n?this.options.supportPointer?at(document,"pointermove",this._onTouchMove):n?at(document,"touchmove",this._onTouchMove):at(document,"mousemove",this._onTouchMove):(at(w,"dragend",this),at(mt,"dragstart",this._onDragStart));try{document.selection?fr(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch{}},_dragStarted:function(e,n){if(we=!1,mt&&w){Ht("dragStarted",this,{evt:n}),this.nativeDraggable&&at(document,"dragover",Ds);var s=this.options;!e&&bt(w,s.dragClass,!1),bt(w,s.ghostClass,!0),Z.active=this,e&&this._appendGhost(),Gt({sortable:this,name:"start",originalEvent:n})}else this._nulling()},_emulateDragOver:function(){if(ne){this._lastX=ne.clientX,this._lastY=ne.clientY,An();for(var e=document.elementFromPoint(ne.clientX,ne.clientY),n=e;e&&e.shadowRoot&&(e=e.shadowRoot.elementFromPoint(ne.clientX,ne.clientY),e!==n);)n=e;if(w.parentNode[Ut]._isOutsideThisEl(e),n)do{if(n[Ut]){var s=void 0;if(s=n[Ut]._onDragOver({clientX:ne.clientX,clientY:ne.clientY,target:e,rootEl:n}),s&&!this.options.dragoverBubble)break}e=n}while(n=n.parentNode);Dn()}},_onTouchMove:function(e){if(xe){var n=this.options,s=n.fallbackTolerance,r=n.fallbackOffset,u=e.touches?e.touches[0]:e,t=rt&&Ie(rt,!0),a=rt&&t&&t.a,i=rt&&t&&t.d,l=rr&&Lt&&tn(Lt),c=(u.clientX-xe.clientX+r.x)/(a||1)+(l?l[0]-Dr[0]:0)/(a||1),f=(u.clientY-xe.clientY+r.y)/(i||1)+(l?l[1]-Dr[1]:0)/(i||1);if(!Z.active&&!we){if(s&&Math.max(Math.abs(u.clientX-this._lastX),Math.abs(u.clientY-this._lastY))=0&&(Gt({rootEl:Et,name:"add",toEl:Et,fromEl:mt,originalEvent:e}),Gt({sortable:this,name:"remove",toEl:Et,originalEvent:e}),Gt({rootEl:Et,name:"sort",toEl:Et,fromEl:mt,originalEvent:e}),Gt({sortable:this,name:"sort",toEl:Et,originalEvent:e})),Mt&&Mt.save()):Zt!==Fe&&Zt>=0&&(Gt({sortable:this,name:"update",toEl:Et,originalEvent:e}),Gt({sortable:this,name:"sort",toEl:Et,originalEvent:e})),Z.active&&((Zt==null||Zt===-1)&&(Zt=Fe,he=ze),Gt({sortable:this,name:"end",toEl:Et,originalEvent:e}),this.save()))),this._nulling()},_nulling:function(){Ht("nulling",this),mt=w=Et=rt=Oe=Ot=ur=ge=xe=ne=Ke=Zt=he=Fe=ze=Re=Je=Mt=tr=Z.dragged=Z.ghost=Z.clone=Z.active=null,mr.forEach(function(e){e.checked=!0}),mr.length=Ir=Ar=0},handleEvent:function(e){switch(e.type){case"drop":case"dragend":this._onDrop(e);break;case"dragenter":case"dragover":w&&(this._onDragOver(e),Rs(e));break;case"selectstart":e.preventDefault();break}},toArray:function(){for(var e=[],n,s=this.el.children,r=0,u=s.length,t=this.options;rs.right+r||o.clientX<=s.right&&o.clientY>s.bottom&&o.clientX>=s.left:o.clientX>s.right&&o.clientY>s.top||o.clientX<=s.right&&o.clientY>s.bottom+r}function js(o,e,n,s,r,u,t,a){var i=s?o.clientY:o.clientX,l=s?n.height:n.width,c=s?n.top:n.left,f=s?n.bottom:n.right,d=!1;if(!t){if(a&&crc+l*u/2:if-cr)return-Je}else if(i>c+l*(1-r)/2&&if-l*u/2)?i>c+l/2?1:-1:0}function Ms(o){return Pt(w)1&&(et.forEach(function(a){u.addAnimationState({target:a,rect:Wt?yt(a):t}),Cr(a),a.fromRect=t,s.removeAnimationState(a)}),Wt=!1,Ks(!this.options.removeCloneOnHide,r))},dragOverCompleted:function(n){var s=n.sortable,r=n.isOwner,u=n.insertion,t=n.activeSortable,a=n.parentEl,i=n.putSortable,l=this.options;if(u){if(r&&t._hideClone(),Ue=!1,l.animation&&et.length>1&&(Wt||!r&&!t.options.sort&&!i)){var c=yt(vt,!1,!0,!0);et.forEach(function(d){d!==vt&&(en(d,c),a.appendChild(d))}),Wt=!0}if(!r)if(Wt||ar(),et.length>1){var f=or;t._showClone(s),t.options.animation&&!or&&f&&Jt.forEach(function(d){t.addAnimationState({target:d,rect:Be}),d.fromRect=Be,d.thisAnimationDuration=null})}else t._showClone(s)}},dragOverAnimationCapture:function(n){var s=n.dragRect,r=n.isOwner,u=n.activeSortable;if(et.forEach(function(a){a.thisAnimationDuration=null}),u.options.animation&&!r&&u.multiDrag.isMultiDrag){Be=ee({},s);var t=Ie(vt,!0);Be.top-=t.f,Be.left-=t.e}},dragOverAnimationComplete:function(){Wt&&(Wt=!1,ar())},drop:function(n){var s=n.originalEvent,r=n.rootEl,u=n.parentEl,t=n.sortable,a=n.dispatchSortableEvent,i=n.oldIndex,l=n.putSortable,c=l||this.sortable;if(s){var f=this.options,d=u.children;if(!$e)if(f.multiDragKey&&!this.multiDragKeyDown&&this._deselectMultiDrag(),bt(vt,f.selectedClass,!~et.indexOf(vt)),~et.indexOf(vt))et.splice(et.indexOf(vt),1),Le=null,Ve({sortable:t,rootEl:r,name:"deselect",targetEl:vt});else{if(et.push(vt),Ve({sortable:t,rootEl:r,name:"select",targetEl:vt}),s.shiftKey&&Le&&t.el.contains(Le)){var v=Pt(Le),p=Pt(vt);if(~v&&~p&&v!==p){var h,g;for(p>v?(g=v,h=p):(g=p,h=v+1);g1){var b=yt(vt),S=Pt(vt,":not(."+this.options.selectedClass+")");if(!Ue&&f.animation&&(vt.thisAnimationDuration=null),c.captureAnimationState(),!Ue&&(f.animation&&(vt.fromRect=b,et.forEach(function(x){if(x.thisAnimationDuration=null,x!==vt){var C=Wt?yt(x):b;x.fromRect=C,c.addAnimationState({target:x,rect:C})}})),ar(),et.forEach(function(x){d[S]?u.insertBefore(x,d[S]):u.appendChild(x),S++}),i===Pt(vt))){var I=!1;et.forEach(function(x){if(x.sortableIndex!==Pt(x)){I=!0;return}}),I&&a("update")}et.forEach(function(x){Cr(x)}),c.animateAll()}oe=c}(r===u||l&&l.lastPutMode!=="clone")&&Jt.forEach(function(x){x.parentNode&&x.parentNode.removeChild(x)})}},nullingGlobal:function(){this.isMultiDrag=$e=!1,Jt.length=0},destroyGlobal:function(){this._deselectMultiDrag(),ot(document,"pointerup",this._deselectMultiDrag),ot(document,"mouseup",this._deselectMultiDrag),ot(document,"touchend",this._deselectMultiDrag),ot(document,"keydown",this._checkKeyDown),ot(document,"keyup",this._checkKeyUp)},_deselectMultiDrag:function(n){if(!(typeof $e<"u"&&$e)&&oe===this.sortable&&!(n&&ae(n.target,this.options.draggable,this.sortable.el,!1))&&!(n&&n.button!==0))for(;et.length;){var s=et[0];bt(s,this.options.selectedClass,!1),et.shift(),Ve({sortable:this.sortable,rootEl:this.sortable.el,name:"deselect",targetEl:s})}},_checkKeyDown:function(n){n.key===this.options.multiDragKey&&(this.multiDragKeyDown=!0)},_checkKeyUp:function(n){n.key===this.options.multiDragKey&&(this.multiDragKeyDown=!1)}},ee(o,{pluginName:"multiDrag",utils:{select:function(n){var s=n.parentNode[Ut];!s||!s.options.multiDrag||~et.indexOf(n)||(oe&&oe!==s&&(oe.multiDrag._deselectMultiDrag(),oe=s),bt(n,s.options.selectedClass,!0),et.push(n))},deselect:function(n){var s=n.parentNode[Ut],r=et.indexOf(n);!s||!s.options.multiDrag||!~r||(bt(n,s.options.selectedClass,!1),et.splice(r,1))}},eventProperties:function(){var n=this,s=[],r=[];return et.forEach(function(u){s.push({multiDragElement:u,index:u.sortableIndex});var t;Wt&&u!==vt?t=-1:Wt?t=Pt(u,":not(."+n.options.selectedClass+")"):t=Pt(u),r.push({multiDragElement:u,index:t})}),{items:fs(et),clones:[].concat(Jt),oldIndicies:s,newIndicies:r}},optionListeners:{multiDragKey:function(n){return n=n.toLowerCase(),n==="ctrl"?n="Control":n.length>1&&(n=n.charAt(0).toUpperCase()+n.substr(1)),n}}})}function Ks(o,e){et.forEach(function(n,s){var r=e.children[n.sortableIndex+(o?Number(s):0)];r?e.insertBefore(n,r):e.appendChild(n)})}function sn(o,e){Jt.forEach(function(n,s){var r=e.children[n.sortableIndex+(o?Number(s):0)];r?e.insertBefore(n,r):e.appendChild(n)})}function ar(){et.forEach(function(o){o!==vt&&o.parentNode&&o.parentNode.removeChild(o)})}Z.mount(new Us);Z.mount(Xr,Wr);const Hs=Object.freeze(Object.defineProperty({__proto__:null,MultiDrag:Vs,Sortable:Z,Swap:Bs,default:Z},Symbol.toStringTag,{value:"Module"})),Ws=bn(Hs);var Xs=sr.exports,ln;function Ys(){return ln||(ln=1,(function(o,e){(function(s,r){o.exports=r(ss,Ws)})(typeof self<"u"?self:Xs,function(n,s){return(function(r){var u={};function t(a){if(u[a])return u[a].exports;var i=u[a]={i:a,l:!1,exports:{}};return r[a].call(i.exports,i,i.exports,t),i.l=!0,i.exports}return t.m=r,t.c=u,t.d=function(a,i,l){t.o(a,i)||Object.defineProperty(a,i,{enumerable:!0,get:l})},t.r=function(a){typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(a,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(a,"__esModule",{value:!0})},t.t=function(a,i){if(i&1&&(a=t(a)),i&8||i&4&&typeof a=="object"&&a&&a.__esModule)return a;var l=Object.create(null);if(t.r(l),Object.defineProperty(l,"default",{enumerable:!0,value:a}),i&2&&typeof a!="string")for(var c in a)t.d(l,c,(function(f){return a[f]}).bind(null,c));return l},t.n=function(a){var i=a&&a.__esModule?function(){return a.default}:function(){return a};return t.d(i,"a",i),i},t.o=function(a,i){return Object.prototype.hasOwnProperty.call(a,i)},t.p="",t(t.s="fb15")})({"00ee":(function(r,u,t){var a=t("b622"),i=a("toStringTag"),l={};l[i]="z",r.exports=String(l)==="[object z]"}),"0366":(function(r,u,t){var a=t("1c0b");r.exports=function(i,l,c){if(a(i),l===void 0)return i;switch(c){case 0:return function(){return i.call(l)};case 1:return function(f){return i.call(l,f)};case 2:return function(f,d){return i.call(l,f,d)};case 3:return function(f,d,v){return i.call(l,f,d,v)}}return function(){return i.apply(l,arguments)}}}),"057f":(function(r,u,t){var a=t("fc6a"),i=t("241c").f,l={}.toString,c=typeof window=="object"&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],f=function(d){try{return i(d)}catch{return c.slice()}};r.exports.f=function(v){return c&&l.call(v)=="[object Window]"?f(v):i(a(v))}}),"06cf":(function(r,u,t){var a=t("83ab"),i=t("d1e7"),l=t("5c6c"),c=t("fc6a"),f=t("c04e"),d=t("5135"),v=t("0cfb"),p=Object.getOwnPropertyDescriptor;u.f=a?p:function(g,b){if(g=c(g),b=f(b,!0),v)try{return p(g,b)}catch{}if(d(g,b))return l(!i.f.call(g,b),g[b])}}),"0cfb":(function(r,u,t){var a=t("83ab"),i=t("d039"),l=t("cc12");r.exports=!a&&!i(function(){return Object.defineProperty(l("div"),"a",{get:function(){return 7}}).a!=7})}),"13d5":(function(r,u,t){var a=t("23e7"),i=t("d58f").left,l=t("a640"),c=t("ae40"),f=l("reduce"),d=c("reduce",{1:0});a({target:"Array",proto:!0,forced:!f||!d},{reduce:function(p){return i(this,p,arguments.length,arguments.length>1?arguments[1]:void 0)}})}),"14c3":(function(r,u,t){var a=t("c6b6"),i=t("9263");r.exports=function(l,c){var f=l.exec;if(typeof f=="function"){var d=f.call(l,c);if(typeof d!="object")throw TypeError("RegExp exec method returned something other than an Object or null");return d}if(a(l)!=="RegExp")throw TypeError("RegExp#exec called on incompatible receiver");return i.call(l,c)}}),"159b":(function(r,u,t){var a=t("da84"),i=t("fdbc"),l=t("17c2"),c=t("9112");for(var f in i){var d=a[f],v=d&&d.prototype;if(v&&v.forEach!==l)try{c(v,"forEach",l)}catch{v.forEach=l}}}),"17c2":(function(r,u,t){var a=t("b727").forEach,i=t("a640"),l=t("ae40"),c=i("forEach"),f=l("forEach");r.exports=!c||!f?function(v){return a(this,v,arguments.length>1?arguments[1]:void 0)}:[].forEach}),"1be4":(function(r,u,t){var a=t("d066");r.exports=a("document","documentElement")}),"1c0b":(function(r,u){r.exports=function(t){if(typeof t!="function")throw TypeError(String(t)+" is not a function");return t}}),"1c7e":(function(r,u,t){var a=t("b622"),i=a("iterator"),l=!1;try{var c=0,f={next:function(){return{done:!!c++}},return:function(){l=!0}};f[i]=function(){return this},Array.from(f,function(){throw 2})}catch{}r.exports=function(d,v){if(!v&&!l)return!1;var p=!1;try{var h={};h[i]=function(){return{next:function(){return{done:p=!0}}}},d(h)}catch{}return p}}),"1d80":(function(r,u){r.exports=function(t){if(t==null)throw TypeError("Can't call method on "+t);return t}}),"1dde":(function(r,u,t){var a=t("d039"),i=t("b622"),l=t("2d00"),c=i("species");r.exports=function(f){return l>=51||!a(function(){var d=[],v=d.constructor={};return v[c]=function(){return{foo:1}},d[f](Boolean).foo!==1})}}),"23cb":(function(r,u,t){var a=t("a691"),i=Math.max,l=Math.min;r.exports=function(c,f){var d=a(c);return d<0?i(d+f,0):l(d,f)}}),"23e7":(function(r,u,t){var a=t("da84"),i=t("06cf").f,l=t("9112"),c=t("6eeb"),f=t("ce4e"),d=t("e893"),v=t("94ca");r.exports=function(p,h){var g=p.target,b=p.global,S=p.stat,I,x,C,P,L,U;if(b?x=a:S?x=a[g]||f(g,{}):x=(a[g]||{}).prototype,x)for(C in h){if(L=h[C],p.noTargetGet?(U=i(x,C),P=U&&U.value):P=x[C],I=v(b?C:g+(S?".":"#")+C,p.forced),!I&&P!==void 0){if(typeof L==typeof P)continue;d(L,P)}(p.sham||P&&P.sham)&&l(L,"sham",!0),c(x,C,L,p)}}}),"241c":(function(r,u,t){var a=t("ca84"),i=t("7839"),l=i.concat("length","prototype");u.f=Object.getOwnPropertyNames||function(f){return a(f,l)}}),"25f0":(function(r,u,t){var a=t("6eeb"),i=t("825a"),l=t("d039"),c=t("ad6d"),f="toString",d=RegExp.prototype,v=d[f],p=l(function(){return v.call({source:"a",flags:"b"})!="/a/b"}),h=v.name!=f;(p||h)&&a(RegExp.prototype,f,function(){var b=i(this),S=String(b.source),I=b.flags,x=String(I===void 0&&b instanceof RegExp&&!("flags"in d)?c.call(b):I);return"/"+S+"/"+x},{unsafe:!0})}),"2ca0":(function(r,u,t){var a=t("23e7"),i=t("06cf").f,l=t("50c4"),c=t("5a34"),f=t("1d80"),d=t("ab13"),v=t("c430"),p="".startsWith,h=Math.min,g=d("startsWith"),b=!v&&!g&&!!(function(){var S=i(String.prototype,"startsWith");return S&&!S.writable})();a({target:"String",proto:!0,forced:!b&&!g},{startsWith:function(I){var x=String(f(this));c(I);var C=l(h(arguments.length>1?arguments[1]:void 0,x.length)),P=String(I);return p?p.call(x,P,C):x.slice(C,C+P.length)===P}})}),"2d00":(function(r,u,t){var a=t("da84"),i=t("342f"),l=a.process,c=l&&l.versions,f=c&&c.v8,d,v;f?(d=f.split("."),v=d[0]+d[1]):i&&(d=i.match(/Edge\/(\d+)/),(!d||d[1]>=74)&&(d=i.match(/Chrome\/(\d+)/),d&&(v=d[1]))),r.exports=v&&+v}),"342f":(function(r,u,t){var a=t("d066");r.exports=a("navigator","userAgent")||""}),"35a1":(function(r,u,t){var a=t("f5df"),i=t("3f8c"),l=t("b622"),c=l("iterator");r.exports=function(f){if(f!=null)return f[c]||f["@@iterator"]||i[a(f)]}}),"37e8":(function(r,u,t){var a=t("83ab"),i=t("9bf2"),l=t("825a"),c=t("df75");r.exports=a?Object.defineProperties:function(d,v){l(d);for(var p=c(v),h=p.length,g=0,b;h>g;)i.f(d,b=p[g++],v[b]);return d}}),"3bbe":(function(r,u,t){var a=t("861d");r.exports=function(i){if(!a(i)&&i!==null)throw TypeError("Can't set "+String(i)+" as a prototype");return i}}),"3ca3":(function(r,u,t){var a=t("6547").charAt,i=t("69f3"),l=t("7dd0"),c="String Iterator",f=i.set,d=i.getterFor(c);l(String,"String",function(v){f(this,{type:c,string:String(v),index:0})},function(){var p=d(this),h=p.string,g=p.index,b;return g>=h.length?{value:void 0,done:!0}:(b=a(h,g),p.index+=b.length,{value:b,done:!1})})}),"3f8c":(function(r,u){r.exports={}}),4160:(function(r,u,t){var a=t("23e7"),i=t("17c2");a({target:"Array",proto:!0,forced:[].forEach!=i},{forEach:i})}),"428f":(function(r,u,t){var a=t("da84");r.exports=a}),"44ad":(function(r,u,t){var a=t("d039"),i=t("c6b6"),l="".split;r.exports=a(function(){return!Object("z").propertyIsEnumerable(0)})?function(c){return i(c)=="String"?l.call(c,""):Object(c)}:Object}),"44d2":(function(r,u,t){var a=t("b622"),i=t("7c73"),l=t("9bf2"),c=a("unscopables"),f=Array.prototype;f[c]==null&&l.f(f,c,{configurable:!0,value:i(null)}),r.exports=function(d){f[c][d]=!0}}),"44e7":(function(r,u,t){var a=t("861d"),i=t("c6b6"),l=t("b622"),c=l("match");r.exports=function(f){var d;return a(f)&&((d=f[c])!==void 0?!!d:i(f)=="RegExp")}}),4930:(function(r,u,t){var a=t("d039");r.exports=!!Object.getOwnPropertySymbols&&!a(function(){return!String(Symbol())})}),"4d64":(function(r,u,t){var a=t("fc6a"),i=t("50c4"),l=t("23cb"),c=function(f){return function(d,v,p){var h=a(d),g=i(h.length),b=l(p,g),S;if(f&&v!=v){for(;g>b;)if(S=h[b++],S!=S)return!0}else for(;g>b;b++)if((f||b in h)&&h[b]===v)return f||b||0;return!f&&-1}};r.exports={includes:c(!0),indexOf:c(!1)}}),"4de4":(function(r,u,t){var a=t("23e7"),i=t("b727").filter,l=t("1dde"),c=t("ae40"),f=l("filter"),d=c("filter");a({target:"Array",proto:!0,forced:!f||!d},{filter:function(p){return i(this,p,arguments.length>1?arguments[1]:void 0)}})}),"4df4":(function(r,u,t){var a=t("0366"),i=t("7b0b"),l=t("9bdd"),c=t("e95a"),f=t("50c4"),d=t("8418"),v=t("35a1");r.exports=function(h){var g=i(h),b=typeof this=="function"?this:Array,S=arguments.length,I=S>1?arguments[1]:void 0,x=I!==void 0,C=v(g),P=0,L,U,O,A,$,W;if(x&&(I=a(I,S>2?arguments[2]:void 0,2)),C!=null&&!(b==Array&&c(C)))for(A=C.call(g),$=A.next,U=new b;!(O=$.call(A)).done;P++)W=x?l(A,I,[O.value,P],!0):O.value,d(U,P,W);else for(L=f(g.length),U=new b(L);L>P;P++)W=x?I(g[P],P):g[P],d(U,P,W);return U.length=P,U}}),"4fad":(function(r,u,t){var a=t("23e7"),i=t("6f53").entries;a({target:"Object",stat:!0},{entries:function(c){return i(c)}})}),"50c4":(function(r,u,t){var a=t("a691"),i=Math.min;r.exports=function(l){return l>0?i(a(l),9007199254740991):0}}),5135:(function(r,u){var t={}.hasOwnProperty;r.exports=function(a,i){return t.call(a,i)}}),5319:(function(r,u,t){var a=t("d784"),i=t("825a"),l=t("7b0b"),c=t("50c4"),f=t("a691"),d=t("1d80"),v=t("8aa5"),p=t("14c3"),h=Math.max,g=Math.min,b=Math.floor,S=/\$([$&'`]|\d\d?|<[^>]*>)/g,I=/\$([$&'`]|\d\d?)/g,x=function(C){return C===void 0?C:String(C)};a("replace",2,function(C,P,L,U){var O=U.REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE,A=U.REPLACE_KEEPS_$0,$=O?"$":"$0";return[function(F,k){var M=d(this),H=F==null?void 0:F[C];return H!==void 0?H.call(F,M,k):P.call(String(M),F,k)},function(R,F){if(!O&&A||typeof F=="string"&&F.indexOf($)===-1){var k=L(P,R,this,F);if(k.done)return k.value}var M=i(R),H=String(this),lt=typeof F=="function";lt||(F=String(F));var dt=M.global;if(dt){var wt=M.unicode;M.lastIndex=0}for(var ht=[];;){var gt=p(M,H);if(gt===null||(ht.push(gt),!dt))break;var Ct=String(gt[0]);Ct===""&&(M.lastIndex=v(H,c(M.lastIndex),wt))}for(var Dt="",St=0,ft=0;ft=St&&(Dt+=H.slice(St,Bt)+Rt,St=Bt+pt.length)}return Dt+H.slice(St)}];function W(R,F,k,M,H,lt){var dt=k+R.length,wt=M.length,ht=I;return H!==void 0&&(H=l(H),ht=S),P.call(lt,ht,function(gt,Ct){var Dt;switch(Ct.charAt(0)){case"$":return"$";case"&":return R;case"`":return F.slice(0,k);case"'":return F.slice(dt);case"<":Dt=H[Ct.slice(1,-1)];break;default:var St=+Ct;if(St===0)return gt;if(St>wt){var ft=b(St/10);return ft===0?gt:ft<=wt?M[ft-1]===void 0?Ct.charAt(1):M[ft-1]+Ct.charAt(1):gt}Dt=M[St-1]}return Dt===void 0?"":Dt})}})}),5692:(function(r,u,t){var a=t("c430"),i=t("c6cd");(r.exports=function(l,c){return i[l]||(i[l]=c!==void 0?c:{})})("versions",[]).push({version:"3.6.5",mode:a?"pure":"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})}),"56ef":(function(r,u,t){var a=t("d066"),i=t("241c"),l=t("7418"),c=t("825a");r.exports=a("Reflect","ownKeys")||function(d){var v=i.f(c(d)),p=l.f;return p?v.concat(p(d)):v}}),"5a34":(function(r,u,t){var a=t("44e7");r.exports=function(i){if(a(i))throw TypeError("The method doesn't accept regular expressions");return i}}),"5c6c":(function(r,u){r.exports=function(t,a){return{enumerable:!(t&1),configurable:!(t&2),writable:!(t&4),value:a}}}),"5db7":(function(r,u,t){var a=t("23e7"),i=t("a2bf"),l=t("7b0b"),c=t("50c4"),f=t("1c0b"),d=t("65f0");a({target:"Array",proto:!0},{flatMap:function(p){var h=l(this),g=c(h.length),b;return f(p),b=d(h,0),b.length=i(b,h,h,g,0,1,p,arguments.length>1?arguments[1]:void 0),b}})}),6547:(function(r,u,t){var a=t("a691"),i=t("1d80"),l=function(c){return function(f,d){var v=String(i(f)),p=a(d),h=v.length,g,b;return p<0||p>=h?c?"":void 0:(g=v.charCodeAt(p),g<55296||g>56319||p+1===h||(b=v.charCodeAt(p+1))<56320||b>57343?c?v.charAt(p):g:c?v.slice(p,p+2):(g-55296<<10)+(b-56320)+65536)}};r.exports={codeAt:l(!1),charAt:l(!0)}}),"65f0":(function(r,u,t){var a=t("861d"),i=t("e8b5"),l=t("b622"),c=l("species");r.exports=function(f,d){var v;return i(f)&&(v=f.constructor,typeof v=="function"&&(v===Array||i(v.prototype))?v=void 0:a(v)&&(v=v[c],v===null&&(v=void 0))),new(v===void 0?Array:v)(d===0?0:d)}}),"69f3":(function(r,u,t){var a=t("7f9a"),i=t("da84"),l=t("861d"),c=t("9112"),f=t("5135"),d=t("f772"),v=t("d012"),p=i.WeakMap,h,g,b,S=function(O){return b(O)?g(O):h(O,{})},I=function(O){return function(A){var $;if(!l(A)||($=g(A)).type!==O)throw TypeError("Incompatible receiver, "+O+" required");return $}};if(a){var x=new p,C=x.get,P=x.has,L=x.set;h=function(O,A){return L.call(x,O,A),A},g=function(O){return C.call(x,O)||{}},b=function(O){return P.call(x,O)}}else{var U=d("state");v[U]=!0,h=function(O,A){return c(O,U,A),A},g=function(O){return f(O,U)?O[U]:{}},b=function(O){return f(O,U)}}r.exports={set:h,get:g,has:b,enforce:S,getterFor:I}}),"6eeb":(function(r,u,t){var a=t("da84"),i=t("9112"),l=t("5135"),c=t("ce4e"),f=t("8925"),d=t("69f3"),v=d.get,p=d.enforce,h=String(String).split("String");(r.exports=function(g,b,S,I){var x=I?!!I.unsafe:!1,C=I?!!I.enumerable:!1,P=I?!!I.noTargetGet:!1;if(typeof S=="function"&&(typeof b=="string"&&!l(S,"name")&&i(S,"name",b),p(S).source=h.join(typeof b=="string"?b:"")),g===a){C?g[b]=S:c(b,S);return}else x?!P&&g[b]&&(C=!0):delete g[b];C?g[b]=S:i(g,b,S)})(Function.prototype,"toString",function(){return typeof this=="function"&&v(this).source||f(this)})}),"6f53":(function(r,u,t){var a=t("83ab"),i=t("df75"),l=t("fc6a"),c=t("d1e7").f,f=function(d){return function(v){for(var p=l(v),h=i(p),g=h.length,b=0,S=[],I;g>b;)I=h[b++],(!a||c.call(p,I))&&S.push(d?[I,p[I]]:p[I]);return S}};r.exports={entries:f(!0),values:f(!1)}}),"73d9":(function(r,u,t){var a=t("44d2");a("flatMap")}),7418:(function(r,u){u.f=Object.getOwnPropertySymbols}),"746f":(function(r,u,t){var a=t("428f"),i=t("5135"),l=t("e538"),c=t("9bf2").f;r.exports=function(f){var d=a.Symbol||(a.Symbol={});i(d,f)||c(d,f,{value:l.f(f)})}}),7839:(function(r,u){r.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]}),"7b0b":(function(r,u,t){var a=t("1d80");r.exports=function(i){return Object(a(i))}}),"7c73":(function(r,u,t){var a=t("825a"),i=t("37e8"),l=t("7839"),c=t("d012"),f=t("1be4"),d=t("cc12"),v=t("f772"),p=">",h="<",g="prototype",b="script",S=v("IE_PROTO"),I=function(){},x=function(O){return h+b+p+O+h+"/"+b+p},C=function(O){O.write(x("")),O.close();var A=O.parentWindow.Object;return O=null,A},P=function(){var O=d("iframe"),A="java"+b+":",$;return O.style.display="none",f.appendChild(O),O.src=String(A),$=O.contentWindow.document,$.open(),$.write(x("document.F=Object")),$.close(),$.F},L,U=function(){try{L=document.domain&&new ActiveXObject("htmlfile")}catch{}U=L?C(L):P();for(var O=l.length;O--;)delete U[g][l[O]];return U()};c[S]=!0,r.exports=Object.create||function(A,$){var W;return A!==null?(I[g]=a(A),W=new I,I[g]=null,W[S]=A):W=U(),$===void 0?W:i(W,$)}}),"7dd0":(function(r,u,t){var a=t("23e7"),i=t("9ed3"),l=t("e163"),c=t("d2bb"),f=t("d44e"),d=t("9112"),v=t("6eeb"),p=t("b622"),h=t("c430"),g=t("3f8c"),b=t("ae93"),S=b.IteratorPrototype,I=b.BUGGY_SAFARI_ITERATORS,x=p("iterator"),C="keys",P="values",L="entries",U=function(){return this};r.exports=function(O,A,$,W,R,F,k){i($,A,W);var M=function(ft){if(ft===R&&ht)return ht;if(!I&&ft in dt)return dt[ft];switch(ft){case C:return function(){return new $(this,ft)};case P:return function(){return new $(this,ft)};case L:return function(){return new $(this,ft)}}return function(){return new $(this)}},H=A+" Iterator",lt=!1,dt=O.prototype,wt=dt[x]||dt["@@iterator"]||R&&dt[R],ht=!I&&wt||M(R),gt=A=="Array"&&dt.entries||wt,Ct,Dt,St;if(gt&&(Ct=l(gt.call(new O)),S!==Object.prototype&&Ct.next&&(!h&&l(Ct)!==S&&(c?c(Ct,S):typeof Ct[x]!="function"&&d(Ct,x,U)),f(Ct,H,!0,!0),h&&(g[H]=U))),R==P&&wt&&wt.name!==P&&(lt=!0,ht=function(){return wt.call(this)}),(!h||k)&&dt[x]!==ht&&d(dt,x,ht),g[A]=ht,R)if(Dt={values:M(P),keys:F?ht:M(C),entries:M(L)},k)for(St in Dt)(I||lt||!(St in dt))&&v(dt,St,Dt[St]);else a({target:A,proto:!0,forced:I||lt},Dt);return Dt}}),"7f9a":(function(r,u,t){var a=t("da84"),i=t("8925"),l=a.WeakMap;r.exports=typeof l=="function"&&/native code/.test(i(l))}),"825a":(function(r,u,t){var a=t("861d");r.exports=function(i){if(!a(i))throw TypeError(String(i)+" is not an object");return i}}),"83ab":(function(r,u,t){var a=t("d039");r.exports=!a(function(){return Object.defineProperty({},1,{get:function(){return 7}})[1]!=7})}),8418:(function(r,u,t){var a=t("c04e"),i=t("9bf2"),l=t("5c6c");r.exports=function(c,f,d){var v=a(f);v in c?i.f(c,v,l(0,d)):c[v]=d}}),"861d":(function(r,u){r.exports=function(t){return typeof t=="object"?t!==null:typeof t=="function"}}),8875:(function(r,u,t){var a,i,l;(function(c,f){i=[],a=f,l=typeof a=="function"?a.apply(u,i):a,l!==void 0&&(r.exports=l)})(typeof self<"u"?self:this,function(){function c(){var f=Object.getOwnPropertyDescriptor(document,"currentScript");if(!f&&"currentScript"in document&&document.currentScript||f&&f.get!==c&&document.currentScript)return document.currentScript;try{throw new Error}catch(L){var d=/.*at [^(]*\((.*):(.+):(.+)\)$/ig,v=/@([^@]*):(\d+):(\d+)\s*$/ig,p=d.exec(L.stack)||v.exec(L.stack),h=p&&p[1]||!1,g=p&&p[2]||!1,b=document.location.href.replace(document.location.hash,""),S,I,x,C=document.getElementsByTagName("script");h===b&&(S=document.documentElement.outerHTML,I=new RegExp("(?:[^\\n]+?\\n){0,"+(g-2)+"}[^<]*']); + }); + + /** @var StructuredDataService $service */ + $action = new InjectStructuredDataAction($service); + + $result = $action->executeForItem($model, 'product'); + + $this->assertSame('', $result); + } + + #[Test] + public function execute_for_item_returns_null_when_resource_not_enabled(): void + { + config()->set('justbetter.structured-data.runway', []); + + $model = new Product; + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock): void { + $mock->shouldNotReceive('getJsonLdScripts'); + }); + + /** @var StructuredDataService $service */ + $action = new InjectStructuredDataAction($service); + + $this->assertNull($action->executeForItem($model, 'product')); + } + + #[Test] + public function execute_for_item_returns_null_when_scripts_empty(): void + { + config()->set('justbetter.structured-data.runway', ['product']); + + $model = new Product; + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock) use ($model): void { + $mock->shouldReceive('getJsonLdScripts') + ->once() + ->with($model, false, 'product') + ->andReturn([]); + }); + + /** @var StructuredDataService $service */ + $action = new InjectStructuredDataAction($service); + + $this->assertNull($action->executeForItem($model, 'product')); + } + + #[Test] + public function execute_for_item_handles_entry(): void + { + config()->set('justbetter.structured-data.collections', ['blog']); + + $collection = CollectionFacade::make('blog'); + $collection->save(); + $entry = (new Entry)->collection($collection)->id('entry-123'); + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock) use ($entry): void { + $mock->shouldReceive('getJsonLdScripts') + ->once() + ->with($entry) + ->andReturn(['']); + }); + + /** @var StructuredDataService $service */ + $action = new InjectStructuredDataAction($service); + + $this->assertSame('', $action->executeForItem($entry)); + } + + #[Test] + public function execute_for_item_handles_term(): void + { + config()->set('justbetter.structured-data.taxonomies', ['categories']); + + /** @var Taxonomy $taxonomy */ + $taxonomy = TaxonomyFacade::make('categories'); + $taxonomy->save(); + + $term = $this->mock(LocalizedTerm::class, function (MockInterface $mock) use ($taxonomy): void { + $mock->shouldReceive('taxonomy')->andReturn($taxonomy); + }); + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock) use ($term): void { + $mock->shouldReceive('getJsonLdScripts') + ->once() + ->with($term) + ->andReturn(['']); + }); + + /** @var StructuredDataService $service */ + $action = new InjectStructuredDataAction($service); + + $this->assertSame('', $action->executeForItem($term)); + } + + #[Test] + public function execute_for_item_returns_null_for_unsupported_type(): void + { + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock): void { + $mock->shouldNotReceive('getJsonLdScripts'); + }); + + /** @var StructuredDataService $service */ + $action = new InjectStructuredDataAction($service); + + /** @phpstan-ignore-next-line argument.type */ + $this->assertNull($action->executeForItem('not-supported')); + } + + #[Test] + public function execute_handles_runway_model_from_uri(): void + { + RunwaySupport::fakeInstalled(true); + config()->set('justbetter.structured-data.runway', ['product']); + + $resource = new Resource; + $resource->resourceHandle = 'product'; + + $model = new Product; + $model->runwayResource = $resource; + + ModelRepository::$findByUriResult = $model; + + URL::shouldReceive('getCurrent')->andReturn('/product-url'); + $site = $this->mock(Site::class, function (MockInterface $mock): void { + $mock->shouldReceive('handle')->andReturn('default'); + }); + SiteFacade::shouldReceive('current')->andReturn($site); + SiteFacade::shouldReceive('multiEnabled')->andReturn(false); + EntryFacade::shouldReceive('findByUri')->andReturn(null); + Term::shouldReceive('findByUri')->andReturn(null); + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock) use ($model): void { + $mock->shouldReceive('getJsonLdScripts') + ->once() + ->with($model, false, 'product') + ->andReturn(['']); + }); + + /** @var StructuredDataService $service */ + $action = new InjectStructuredDataAction($service); + + $this->assertSame('', $action->execute()); + } + + #[Test] + public function execute_returns_null_when_runway_uri_model_not_enabled(): void + { + RunwaySupport::fakeInstalled(true); + config()->set('justbetter.structured-data.runway', []); + + $model = new Product; + + ModelRepository::$findByUriResult = $model; + + URL::shouldReceive('getCurrent')->andReturn('/product-url'); + $site = $this->mock(Site::class, function (MockInterface $mock): void { + $mock->shouldReceive('handle')->andReturn('default'); + }); + SiteFacade::shouldReceive('current')->andReturn($site); + SiteFacade::shouldReceive('multiEnabled')->andReturn(false); + EntryFacade::shouldReceive('findByUri')->andReturn(null); + Term::shouldReceive('findByUri')->andReturn(null); + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock): void { + $mock->shouldNotReceive('getJsonLdScripts'); + }); + + /** @var StructuredDataService $service */ + $action = new InjectStructuredDataAction($service); + + $this->assertNull($action->execute()); + } + + #[Test] + public function execute_returns_null_when_runway_uri_result_is_not_model(): void + { + RunwaySupport::fakeInstalled(true); + ModelRepository::$findByUriResult = 'not-a-model'; + + URL::shouldReceive('getCurrent')->andReturn('/product-url'); + $site = $this->mock(Site::class, function (MockInterface $mock): void { + $mock->shouldReceive('handle')->andReturn('default'); + }); + SiteFacade::shouldReceive('current')->andReturn($site); + SiteFacade::shouldReceive('multiEnabled')->andReturn(false); + EntryFacade::shouldReceive('findByUri')->andReturn(null); + Term::shouldReceive('findByUri')->andReturn(null); + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock): void { + $mock->shouldNotReceive('getJsonLdScripts'); + }); + + /** @var StructuredDataService $service */ + $action = new InjectStructuredDataAction($service); + + $this->assertNull($action->execute()); + } +} diff --git a/tests/Unit/Actions/InjectStructuredDataActionTest.php b/tests/Unit/Actions/InjectStructuredDataActionTest.php index 6583684..d71d791 100644 --- a/tests/Unit/Actions/InjectStructuredDataActionTest.php +++ b/tests/Unit/Actions/InjectStructuredDataActionTest.php @@ -258,137 +258,4 @@ public function execute_returns_imploded_scripts(): void $this->assertEquals("\n", $result); } - - #[Test] - public function get_current_entry_returns_entry(): void - { - $collection = CollectionFacade::make('blog'); - $collection->save(); - $entry = (new Entry)->collection($collection)->id('entry-123'); - - URL::shouldReceive('getCurrent')->andReturn('/test-url'); - $site = $this->mock(Site::class, function (MockInterface $mock): void { - $mock->shouldReceive('handle')->andReturn('default'); - }); - SiteFacade::shouldReceive('current')->andReturn($site); - SiteFacade::shouldReceive('multiEnabled')->andReturn(false); - EntryFacade::shouldReceive('findByUri')->with('/test-url', 'default')->andReturn($entry); - - /** @var StructuredDataService $service */ - $service = $this->mock(StructuredDataService::class); - /** @var StructuredDataService $service */ - $action = new InjectStructuredDataAction($service); - - $reflection = new \ReflectionClass($action); - $method = $reflection->getMethod('getCurrentEntry'); - $method->setAccessible(true); - $result = $method->invoke($action); - - $this->assertInstanceOf(Entry::class, $result); - } - - #[Test] - public function get_current_entry_returns_page(): void - { - $page = $this->mock(Page::class); - - URL::shouldReceive('getCurrent')->andReturn('/test-url'); - $site = $this->mock(Site::class, function (MockInterface $mock): void { - $mock->shouldReceive('handle')->andReturn('default'); - }); - SiteFacade::shouldReceive('current')->andReturn($site); - SiteFacade::shouldReceive('multiEnabled')->andReturn(false); - EntryFacade::shouldReceive('findByUri')->with('/test-url', 'default')->andReturn($page); - - /** @var StructuredDataService $service */ - $service = $this->mock(StructuredDataService::class); - /** @var StructuredDataService $service */ - $action = new InjectStructuredDataAction($service); - - $reflection = new \ReflectionClass($action); - $method = $reflection->getMethod('getCurrentEntry'); - $method->setAccessible(true); - $result = $method->invoke($action); - - $this->assertInstanceOf(Page::class, $result); - } - - #[Test] - public function get_current_entry_returns_null_when_not_entry_or_page(): void - { - URL::shouldReceive('getCurrent')->andReturn('/test-url'); - $site = $this->mock(Site::class, function (MockInterface $mock): void { - $mock->shouldReceive('handle')->andReturn('default'); - }); - SiteFacade::shouldReceive('current')->andReturn($site); - SiteFacade::shouldReceive('multiEnabled')->andReturn(false); - EntryFacade::shouldReceive('findByUri')->with('/test-url', 'default')->andReturn(null); - - /** @var StructuredDataService $service */ - $service = $this->mock(StructuredDataService::class); - /** @var StructuredDataService $service */ - $action = new InjectStructuredDataAction($service); - - $reflection = new \ReflectionClass($action); - $method = $reflection->getMethod('getCurrentEntry'); - $method->setAccessible(true); - $result = $method->invoke($action); - - $this->assertNull($result); - } - - #[Test] - public function get_current_term_returns_localized_term(): void - { - /** @var Taxonomy $taxonomy */ - $taxonomy = TaxonomyFacade::make('categories'); - $taxonomy->save(); - $term = $this->mock(LocalizedTerm::class, function (MockInterface $mock) use ($taxonomy): void { - $mock->shouldReceive('taxonomy')->andReturn($taxonomy); - }); - - URL::shouldReceive('getCurrent')->andReturn('/test-url'); - $site = $this->mock(Site::class, function (MockInterface $mock): void { - $mock->shouldReceive('handle')->andReturn('default'); - }); - SiteFacade::shouldReceive('current')->andReturn($site); - SiteFacade::shouldReceive('multiEnabled')->andReturn(false); - Term::shouldReceive('findByUri')->with('/test-url', 'default')->andReturn($term); - - /** @var StructuredDataService $service */ - $service = $this->mock(StructuredDataService::class); - /** @var StructuredDataService $service */ - $action = new InjectStructuredDataAction($service); - - $reflection = new \ReflectionClass($action); - $method = $reflection->getMethod('getCurrentTerm'); - $method->setAccessible(true); - $result = $method->invoke($action); - - $this->assertInstanceOf(LocalizedTerm::class, $result); - } - - #[Test] - public function get_current_term_returns_null_when_not_localized_term(): void - { - URL::shouldReceive('getCurrent')->andReturn('/test-url'); - $site = $this->mock(Site::class, function (MockInterface $mock): void { - $mock->shouldReceive('handle')->andReturn('default'); - }); - SiteFacade::shouldReceive('current')->andReturn($site); - SiteFacade::shouldReceive('multiEnabled')->andReturn(false); - Term::shouldReceive('findByUri')->with('/test-url', 'default')->andReturn(null); - - /** @var StructuredDataService $service */ - $service = $this->mock(StructuredDataService::class); - /** @var StructuredDataService $service */ - $action = new InjectStructuredDataAction($service); - - $reflection = new \ReflectionClass($action); - $method = $reflection->getMethod('getCurrentTerm'); - $method->setAccessible(true); - $result = $method->invoke($action); - - $this->assertNull($result); - } } diff --git a/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php b/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php index e2dbe42..163564c 100644 --- a/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php +++ b/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php @@ -4,23 +4,9 @@ use Justbetter\StatamicStructuredData\Fieldtypes\AvailableVariablesFieldtype; use Justbetter\StatamicStructuredData\Tests\TestCase; -use Mockery; use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; -use Statamic\Entries\Collection; -use Statamic\Entries\Entry; -use Statamic\Facades\Blueprint as BlueprintFacade; -use Statamic\Facades\Collection as CollectionFacade; -use Statamic\Facades\Fieldset as FieldsetFacade; -use Statamic\Facades\GlobalSet; -use Statamic\Facades\GlobalSet as GlobalSetFacade; -use Statamic\Facades\Taxonomy as TaxonomyFacade; -use Statamic\Fields\Blueprint; use Statamic\Fields\Field; -use Statamic\Fields\Fields; -use Statamic\Fields\Fieldset; -use Statamic\Globals\GlobalCollection; -use Statamic\Taxonomies\Taxonomy; class AvailableVariablesFieldtypeTest extends TestCase { @@ -59,1289 +45,20 @@ public function preload_returns_variables(): void } #[Test] - public function get_entry_fields_returns_empty_when_no_collection(): void + public function preload_includes_runway_variables_key(): void { $fieldtype = new AvailableVariablesFieldtype; - - /** @var Field $fieldMock */ $fieldMock = $this->mock(Field::class, function (MockInterface $mock): void { $mock->shouldReceive('parent')->andReturn(null); }); - $fieldtype->setField($fieldMock); - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getEntryFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - $this->assertEmpty($result); - } - - #[Test] - public function get_entry_fields_handles_no_blueprints_gracefully(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $collection = CollectionFacade::make('test'); - $collection->save(); - - $templatesCollection = CollectionFacade::make('structured_data_templates'); - $templatesCollection->save(); - $entry = (new Entry) - ->collection($templatesCollection) - ->id('template-123'); - - $collectionMock = Mockery::mock($collection)->makePartial(); - $collectionMock->shouldReceive('entryBlueprints')->andReturn(collect([])); - - $entry->use_for_collection = $collectionMock; - $entryMock = $entry; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { - $mock->shouldReceive('parent')->andReturn($entryMock); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getEntryFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - } - - #[Test] - public function get_term_fields_returns_empty_when_no_taxonomy(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock): void { - $mock->shouldReceive('parent')->andReturn(null); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getTermFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - $this->assertEmpty($result); - } - - #[Test] - public function get_term_fields_handles_no_blueprints_gracefully(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - /** @var Taxonomy $taxonomy */ - $taxonomy = TaxonomyFacade::make('test'); - $taxonomy->save(); - - $templatesCollection = CollectionFacade::make('structured_data_templates'); - $templatesCollection->save(); - $entry = (new Entry) - ->collection($templatesCollection) - ->id('template-123'); - - $taxonomyMock = Mockery::mock($taxonomy)->makePartial(); - $taxonomyMock->shouldReceive('termBlueprints')->andReturn(collect([])); - - $entry->use_for_taxonomy = $taxonomyMock; - $entryMock = $entry; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { - $mock->shouldReceive('parent')->andReturn($entryMock); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getTermFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - } - - #[Test] - public function field_type_is_eligible_returns_true_for_eligible_types(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('fieldTypeIsEligible'); - $method->setAccessible(true); - - $this->assertTrue($method->invoke($fieldtype, 'text')); - $this->assertTrue($method->invoke($fieldtype, 'assets')); - $this->assertTrue($method->invoke($fieldtype, 'bard')); - $this->assertTrue($method->invoke($fieldtype, 'toggle')); - $this->assertTrue($method->invoke($fieldtype, 'integer')); - $this->assertTrue($method->invoke($fieldtype, 'slug')); - $this->assertTrue($method->invoke($fieldtype, 'date')); - $this->assertTrue($method->invoke($fieldtype, 'entries')); - $this->assertTrue($method->invoke($fieldtype, 'aardvark_seo_meta_title')); - $this->assertTrue($method->invoke($fieldtype, 'aardvark_seo_meta_description')); - } - - #[Test] - public function field_type_is_eligible_returns_false_for_ineligible_types(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('fieldTypeIsEligible'); - $method->setAccessible(true); - - $this->assertFalse($method->invoke($fieldtype, 'unknown_type')); - $this->assertFalse($method->invoke($fieldtype, 'grid')); - $this->assertFalse($method->invoke($fieldtype, 'replicator')); - } - - #[Test] - public function get_global_variables_returns_variables(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $blueprint = BlueprintFacade::make('test'); - $blueprint->setContents([ - 'fields' => [ - [ - 'handle' => 'test_field', - 'field' => [ - 'type' => 'text', - 'display' => 'Test Field', - ], - ], - ], - ]); - $blueprint->save(); - - $globalSet = GlobalSetFacade::make('test'); - $globalSet->save(); - $globalSet->blueprint('test'); - - $fieldsMock = $this->mock(Fields::class, function (MockInterface $mock): void { - $mock->shouldReceive('items')->andReturn(collect([ - [ - 'handle' => 'test_field', - 'field' => [ - 'type' => 'text', - 'display' => 'Test Field', - ], - ], - ])); - }); - - $blueprintMock = $this->mock(Blueprint::class, function (MockInterface $mock) use ($fieldsMock): void { - $mock->shouldReceive('fields')->andReturn($fieldsMock); - }); - - $globalSetMock = Mockery::mock($globalSet)->makePartial(); - $globalSetMock->shouldReceive('blueprint')->andReturn($blueprintMock); - $globalSetMock->shouldReceive('handle')->andReturn('test'); - - $globalCollection = new GlobalCollection([$globalSetMock]); - GlobalSet::shouldReceive('all')->andReturn($globalCollection); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getGlobalVariables'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - $this->assertNotEmpty($result); - } - - #[Test] - public function set_field_data_returns_null_for_ineligible_field(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('setFieldData'); - $method->setAccessible(true); - - $field = [ - 'handle' => 'test', - 'field' => [ - 'type' => 'grid', - ], - ]; - - $result = $method->invoke($fieldtype, $field); - - $this->assertNull($result); - } - - #[Test] - public function set_field_data_returns_null_when_handle_is_parent(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('setFieldData'); - $method->setAccessible(true); - - $field = [ - 'handle' => 'parent', - 'field' => [ - 'type' => 'text', - ], - ]; - - $result = $method->invoke($fieldtype, $field); - - $this->assertNull($result); - } - - #[Test] - public function set_field_data_returns_field_data_for_eligible_field(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('setFieldData'); - $method->setAccessible(true); - - $field = [ - 'handle' => 'test_field', - 'field' => [ - 'type' => 'text', - 'display' => 'Test Field', - ], - ]; - - $result = $method->invoke($fieldtype, $field); - - $this->assertIsArray($result); - /** @var array $result */ - $this->assertArrayHasKey('name', $result); - $this->assertArrayHasKey('description', $result); - $this->assertArrayHasKey('children', $result); - $this->assertEquals('test_field', $result['name']); - } - - #[Test] - public function get_collection_variables_returns_empty_when_no_collection_handle(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getCollectionVariables'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype, '', []); - - $this->assertIsArray($result); - $this->assertEmpty($result); - } - - #[Test] - public function get_collection_variables_returns_empty_when_handle_is_structured_data_templates(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getCollectionVariables'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype, 'structured_data_templates', []); - - $this->assertIsArray($result); - $this->assertEmpty($result); - } - - #[Test] - public function get_collection_variables_returns_empty_when_collection_not_found(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - CollectionFacade::shouldReceive('find')->with('nonexistent')->andReturn(null); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getCollectionVariables'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype, 'nonexistent', []); - - $this->assertIsArray($result); - $this->assertEmpty($result); - } - - #[Test] - public function get_collection_variables_returns_variables_when_collection_exists(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $collection = CollectionFacade::make('blog'); - $collection->save(); - $blueprint = BlueprintFacade::make('blog'); - $blueprint->setContents([ - 'fields' => [ - [ - 'handle' => 'title', - 'field' => [ - 'type' => 'text', - 'display' => 'Title', - ], - ], - ], - ]); - $blueprint->save(); - $collection->entryBlueprints(); - - $field = [ - 'handle' => 'related_entry', - 'field' => [ - 'type' => 'entries', - 'display' => 'Related Entry', - 'collections' => ['blog'], - ], - ]; - - CollectionFacade::shouldReceive('find')->with('blog')->andReturn($collection); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getCollectionVariables'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype, 'blog', $field); - - $this->assertIsArray($result); - $this->assertNotEmpty($result); - } - - #[Test] - public function get_collection_variables_returns_empty_when_no_blueprint(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $collectionMock = $this->mock(Collection::class, function ($mock): void { - $mock->shouldReceive('entryBlueprints')->andReturn(collect([])); - }); - - $field = [ - 'handle' => 'related_entry', - 'field' => [ - 'type' => 'entries', - 'display' => 'Related Entry', - 'collections' => ['blog'], - ], - ]; - - CollectionFacade::shouldReceive('find')->with('blog')->andReturn($collectionMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getCollectionVariables'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype, 'blog', $field); - - $this->assertIsArray($result); - $this->assertEmpty($result); - } - - #[Test] - public function set_field_data_returns_field_with_children_for_entries_field_type(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $collection = CollectionFacade::make('blog'); - $collection->save(); - $blueprint = BlueprintFacade::make('blog'); - $blueprint->setContents([ - 'fields' => [ - [ - 'handle' => 'title', - 'field' => [ - 'type' => 'text', - 'display' => 'Title', - ], - ], - ], - ]); - $blueprint->save(); - $collection->entryBlueprints(); - - CollectionFacade::shouldReceive('find')->with('blog')->andReturn($collection); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('setFieldData'); - $method->setAccessible(true); - - $field = [ - 'handle' => 'related_entry', - 'field' => [ - 'type' => 'entries', - 'display' => 'Related Entry', - 'collections' => ['blog'], - ], - ]; - - $result = $method->invoke($fieldtype, $field); - - $this->assertIsArray($result); - /** @var array $result */ - $this->assertArrayHasKey('name', $result); - $this->assertArrayHasKey('description', $result); - $this->assertArrayHasKey('children', $result); - $this->assertNotEmpty($result['children']); - } - - #[Test] - public function set_field_data_returns_null_when_entries_field_has_no_collections(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('setFieldData'); - $method->setAccessible(true); - - $field = [ - 'handle' => 'related_entry', - 'field' => [ - 'type' => 'entries', - 'display' => 'Related Entry', - ], - ]; - - $result = $method->invoke($fieldtype, $field); - - $this->assertNull($result); - } - - #[Test] - public function set_field_data_returns_group_field_with_supported_children(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('setFieldData'); - $method->setAccessible(true); - - $field = [ - 'handle' => 'media_options', - 'field' => [ - 'type' => 'group', - 'display' => 'Media opties', - 'fields' => [ - [ - 'handle' => 'object_fit', - 'field' => [ - 'type' => 'select', - 'display' => 'Object fit', - ], - ], - [ - 'handle' => 'video', - 'field' => [ - 'type' => 'toggle', - 'display' => 'Video', - ], - ], - [ - 'handle' => 'layout', - 'field' => [ - 'type' => 'grid', - 'display' => 'Layout', - ], - ], - ], - ], - ]; - - $result = $method->invoke($fieldtype, $field); - - $this->assertIsArray($result); - /** @var array $result */ - $this->assertSame('media_options', $result['name']); - $this->assertSame('Media opties', $result['description']); - $this->assertArrayHasKey('children', $result); - $this->assertIsArray($result['children']); - $this->assertCount(2, $result['children']); - - /** @var array> $children */ - $children = $result['children']; - $childNames = array_column($children, 'name'); - $childDescriptions = array_column($children, 'description'); - - $this->assertContains('media_options:object_fit', $childNames); - $this->assertContains('Media opties: Object fit', $childDescriptions); - $this->assertContains('media_options:video', $childNames); - $this->assertContains('Media opties: Video', $childDescriptions); - $this->assertNotContains('media_options:layout', $childNames); - } - - #[Test] - public function set_field_data_returns_null_for_group_without_supported_children(): void - { - $fieldtype = new AvailableVariablesFieldtype; - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('setFieldData'); - $method->setAccessible(true); - - $field = [ - 'handle' => 'media_options', - 'field' => [ - 'type' => 'group', - 'display' => 'Media opties', - 'fields' => [ - [ - 'handle' => 'layout', - 'field' => [ - 'type' => 'grid', - 'display' => 'Layout', - ], - ], - ], - ], - ]; - - $result = $method->invoke($fieldtype, $field); - - $this->assertNull($result); - } - - #[Test] - public function normalize_blueprint_items_converts_collection_to_array(): void - { - $fieldtype = new AvailableVariablesFieldtype; - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('normalizeBlueprintItems'); - $method->setAccessible(true); - - $collection = collect([ - ['handle' => 'test', 'field' => ['type' => 'text']], - ]); - - $result = $method->invoke($fieldtype, $collection); - - $this->assertIsArray($result); - $this->assertCount(1, $result); - /** @var array $firstResult */ - $firstResult = $result[0]; - $this->assertEquals('test', $firstResult['handle']); - } - - #[Test] - public function normalize_blueprint_items_returns_array_as_is(): void - { - $fieldtype = new AvailableVariablesFieldtype; - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('normalizeBlueprintItems'); - $method->setAccessible(true); - - $input = [ - ['handle' => 'test', 'field' => ['type' => 'text']], - ]; - - $result = $method->invoke($fieldtype, $input); - - $this->assertIsArray($result); - $this->assertEquals($input, $result); - } - - #[Test] - public function normalize_blueprint_items_returns_empty_array_for_invalid_input(): void - { - $fieldtype = new AvailableVariablesFieldtype; - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('normalizeBlueprintItems'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype, 'not-an-array'); - - $this->assertIsArray($result); - $this->assertEmpty($result); - } - - #[Test] - public function normalize_blueprint_items_includes_imported_fieldset_items(): void - { - $fieldtype = new AvailableVariablesFieldtype; - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('normalizeBlueprintItems'); - $method->setAccessible(true); - - $fieldsetFields = $this->mock(Fields::class, function (MockInterface $mock): void { - $mock->shouldReceive('items')->andReturn([ - [ - 'handle' => 'imported_title', - 'field' => [ - 'type' => 'text', - 'display' => 'Imported Title', - ], - ], - ]); - }); - - $fieldset = $this->mock(Fieldset::class, function (MockInterface $mock) use ($fieldsetFields): void { - $mock->shouldReceive('fields')->andReturn($fieldsetFields); - }); - - FieldsetFacade::shouldReceive('find')->with('seo_fields')->andReturn($fieldset); - - $result = $method->invoke($fieldtype, [ - ['import' => 'seo_fields'], - ]); - - $this->assertIsArray($result); - $this->assertCount(1, $result); - /** @var array $firstResult */ - $firstResult = $result[0]; - $this->assertSame('imported_title', $firstResult['handle']); - } - - #[Test] - public function normalize_blueprint_items_handles_missing_imported_fieldset(): void - { - $fieldtype = new AvailableVariablesFieldtype; - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('normalizeBlueprintItems'); - $method->setAccessible(true); - - FieldsetFacade::shouldReceive('find')->with('missing_fields')->andReturn(null); - - $result = $method->invoke($fieldtype, [ - ['import' => 'missing_fields'], - ]); - - $this->assertIsArray($result); - $this->assertEmpty($result); - } - - #[Test] - public function normalize_blueprint_items_applies_prefix_to_imported_fieldset_items(): void - { - $fieldtype = new AvailableVariablesFieldtype; - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('normalizeBlueprintItems'); - $method->setAccessible(true); - - $fieldsetFields = $this->mock(Fields::class, function (MockInterface $mock): void { - $mock->shouldReceive('items')->andReturn([ - [ - 'handle' => 'image', - 'field' => [ - 'type' => 'assets', - 'display' => 'Image', - ], - ], - ]); - }); - - $fieldset = $this->mock(Fieldset::class, function (MockInterface $mock) use ($fieldsetFields): void { - $mock->shouldReceive('fields')->andReturn($fieldsetFields); - }); - - FieldsetFacade::shouldReceive('find')->with('media')->andReturn($fieldset); - - $result = $method->invoke($fieldtype, [ - ['import' => 'media', 'prefix' => 'featured_'], - ]); - - $this->assertIsArray($result); - $this->assertCount(1, $result); - /** @var array $firstResult */ - $firstResult = $result[0]; - $this->assertSame('featured_image', $firstResult['handle']); - } - - #[Test] - public function normalize_blueprint_items_handles_nested_support_collection_and_invalid_items(): void - { - $fieldtype = new AvailableVariablesFieldtype; - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('normalizeBlueprintItems'); - $method->setAccessible(true); - - $nestedFields = collect([ - [ - 'handle' => 'nested_title', - 'field' => [ - 'type' => 'text', - 'display' => 'Nested Title', - ], - ], - ]); - - $items = [ - collect([ - 'handle' => 'wrapped_field', - 'field' => [ - 'type' => 'text', - 'display' => 'Wrapped Field', - ], - ]), - 'invalid-item', - [ - 'fields' => $nestedFields, - ], - ]; - - $result = $method->invoke($fieldtype, $items); - - $this->assertIsArray($result); - $fieldHandles = array_column($result, 'handle'); - $this->assertContains('wrapped_field', $fieldHandles); - $this->assertContains('nested_title', $fieldHandles); - } - - #[Test] - public function normalize_blueprint_items_handles_invalid_import_handle(): void - { - $fieldtype = new AvailableVariablesFieldtype; - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('normalizeBlueprintItems'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype, [ - ['import' => null], - ]); - - $this->assertIsArray($result); - $this->assertEmpty($result); - } - - #[Test] - public function get_imported_fieldset_items_returns_empty_for_invalid_import_config(): void - { - $fieldtype = new AvailableVariablesFieldtype; - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getImportedFieldsetItems'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype, ['import' => null]); - - $this->assertIsArray($result); - $this->assertEmpty($result); - } - - #[Test] - public function get_entry_fields_includes_base_fields_and_maps_collection_fields(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $collection = CollectionFacade::make('test'); - $collection->save(); - $blueprint = BlueprintFacade::make('test'); - $blueprint->setNamespace('collections.test'); - $blueprint->setContents([ - 'fields' => [ - [ - 'handle' => 'title', - 'field' => [ - 'type' => 'text', - 'display' => 'Title', - ], - ], - ], - ]); - $blueprint->save(); - $collection->entryBlueprints(); - - $templatesCollection = CollectionFacade::make('structured_data_templates'); - $templatesCollection->save(); - $entry = (new Entry) - ->collection($templatesCollection) - ->id('template-123'); - - $entry->use_for_collection = $collection; - $entryMock = $entry; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { - $mock->shouldReceive('parent')->andReturn($entryMock); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getEntryFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - $this->assertNotEmpty($result); - /** @var array $firstResult */ - $firstResult = $result[0]; - $this->assertEquals('absolute_url', $firstResult['name']); - $this->assertGreaterThanOrEqual(2, count($result)); - $fieldNames = array_column($result, 'name'); - $this->assertContains('title', $fieldNames); - } - - #[Test] - public function set_field_data_group_skips_children_without_handle(): void - { - $fieldtype = new AvailableVariablesFieldtype; - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('setFieldData'); - $method->setAccessible(true); - - $field = [ - 'handle' => 'media_options', - 'field' => [ - 'type' => 'group', - 'display' => 'Media opties', - 'fields' => [ - [ - 'field' => [ - 'type' => 'toggle', - 'display' => 'Missing Handle', - ], - ], - [ - 'handle' => 'video', - 'field' => [ - 'type' => 'toggle', - 'display' => 'Video', - ], - ], - ], - ], - ]; - - $result = $method->invoke($fieldtype, $field); + $result = $fieldtype->preload(); - $this->assertIsArray($result); /** @var array $result */ - $this->assertArrayHasKey('children', $result); - /** @var array> $children */ - $children = $result['children']; - $this->assertCount(1, $children); - $this->assertSame('media_options:video', $children[0]['name'] ?? null); - } - - #[Test] - public function get_entry_fields_handles_empty_fields_after_merge_gracefully(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $collection = CollectionFacade::make('test'); - $collection->save(); - $blueprint = $this->mock(Blueprint::class); - $fieldsMock = $this->mock(Fields::class, function (MockInterface $mock): void { - $mock->shouldReceive('items')->andReturn([]); - }); - $blueprint->shouldReceive('fields')->andReturn($fieldsMock); - - $collectionMock = Mockery::mock($collection)->makePartial(); - $collectionMock->shouldReceive('entryBlueprints')->andReturn(collect([$blueprint])); - - $templatesCollection = CollectionFacade::make('structured_data_templates'); - $templatesCollection->save(); - $entry = (new Entry) - ->collection($templatesCollection) - ->id('template-123'); - - $entry->use_for_collection = $collectionMock; - $entryMock = $entry; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { - $mock->shouldReceive('parent')->andReturn($entryMock); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getEntryFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - } - - #[Test] - public function get_term_fields_handles_null_first_blueprint_gracefully(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - /** @var Taxonomy $taxonomy */ - $taxonomy = TaxonomyFacade::make('test'); - $taxonomy->save(); - - $templatesCollection = CollectionFacade::make('structured_data_templates'); - $templatesCollection->save(); - $entry = (new Entry) - ->collection($templatesCollection) - ->id('template-123'); - - $taxonomyMock = Mockery::mock($taxonomy)->makePartial(); - $taxonomyMock->shouldReceive('termBlueprints')->andReturn(collect([null])); - - $entry->use_for_taxonomy = $taxonomyMock; - $entryMock = $entry; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { - $mock->shouldReceive('parent')->andReturn($entryMock); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getTermFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - } - - #[Test] - public function get_term_fields_handles_empty_fields_after_merge_gracefully(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - /** @var Taxonomy $taxonomy */ - $taxonomy = TaxonomyFacade::make('test'); - $taxonomy->save(); - $blueprint = $this->mock(Blueprint::class); - $fieldsMock = $this->mock(Fields::class, function (MockInterface $mock): void { - $mock->shouldReceive('items')->andReturn([]); - }); - $blueprint->shouldReceive('fields')->andReturn($fieldsMock); - - $taxonomyMock = Mockery::mock($taxonomy)->makePartial(); - $taxonomyMock->shouldReceive('termBlueprints')->andReturn(collect([$blueprint])); - - $templatesCollection = CollectionFacade::make('structured_data_templates'); - $templatesCollection->save(); - $entry = (new Entry) - ->collection($templatesCollection) - ->id('template-123'); - - $entry->use_for_taxonomy = $taxonomyMock; - $entryMock = $entry; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { - $mock->shouldReceive('parent')->andReturn($entryMock); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getTermFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - } - - #[Test] - public function get_term_fields_includes_base_fields_and_maps_taxonomy_fields(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - /** @var Taxonomy $taxonomy */ - $taxonomy = TaxonomyFacade::make('test'); - $taxonomy->save(); - $blueprint = BlueprintFacade::make('test'); - $blueprint->setNamespace('taxonomies.test'); - $blueprint->setContents([ - 'fields' => [ - [ - 'handle' => 'title', - 'field' => [ - 'type' => 'text', - 'display' => 'Title', - ], - ], - ], - ]); - $blueprint->save(); - /** @phpstan-ignore-next-line */ - $taxonomy->termBlueprints(['test']); - - $templatesCollection = CollectionFacade::make('structured_data_templates'); - $templatesCollection->save(); - $entry = (new Entry) - ->collection($templatesCollection) - ->id('template-123'); - - $entry->use_for_taxonomy = $taxonomy; - $entryMock = $entry; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { - $mock->shouldReceive('parent')->andReturn($entryMock); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getTermFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - $this->assertNotEmpty($result); - /** @var array $firstResult */ - $firstResult = $result[0]; - $this->assertEquals('absolute_url', $firstResult['name']); - $this->assertGreaterThanOrEqual(2, count($result)); - $fieldNames = array_column($result, 'name'); - $this->assertContains('title', $fieldNames); - } - - #[Test] - public function get_seo_pro_variables_returns_core_cascaded_fields(): void - { - $fieldtype = new AvailableVariablesFieldtype; - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getSeoProVariables'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - /** @var array> $result */ - $this->assertCount(7, $result); - $names = array_column($result, 'name'); - $this->assertSame([ - 'seo:title', - 'seo:compiled_title', - 'seo:description', - 'seo:canonical_url', - 'seo:og_title', - 'seo:og_type', - 'seo:image', - ], $names); - } - - #[Test] - public function get_entry_fields_appends_seo_pro_variables_when_seo_pro_is_available(): void - { - $fieldtype = new class extends AvailableVariablesFieldtype - { - protected function seoProIsInstalled(): bool - { - return true; - } - }; - - $collection = CollectionFacade::make('test'); - $collection->save(); - $blueprint = BlueprintFacade::make('test'); - $blueprint->setNamespace('collections.test'); - $blueprint->setContents([ - 'fields' => [ - [ - 'handle' => 'title', - 'field' => [ - 'type' => 'text', - 'display' => 'Title', - ], - ], - ], - ]); - $blueprint->save(); - $collection->entryBlueprints(); - - $templatesCollection = CollectionFacade::make('structured_data_templates'); - $templatesCollection->save(); - $entry = (new Entry) - ->collection($templatesCollection) - ->id('template-123'); - - $entry->use_for_collection = $collection; - $entryMock = $entry; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { - $mock->shouldReceive('parent')->andReturn($entryMock); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getEntryFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - /** @var array> $result */ - $fieldNames = array_column($result, 'name'); - - $this->assertContains('seo:compiled_title', $fieldNames); - - $titleIndex = array_search('title', $fieldNames, true); - $seoTitleIndex = array_search('seo:title', $fieldNames, true); - - $this->assertNotFalse($titleIndex); - $this->assertNotFalse($seoTitleIndex); - $this->assertGreaterThan($titleIndex, $seoTitleIndex); - } - - #[Test] - public function get_entry_fields_omits_seo_pro_variables_when_seo_is_disabled_for_collection(): void - { - $fieldtype = new class extends AvailableVariablesFieldtype - { - protected function seoProIsInstalled(): bool - { - return true; - } - }; - - $collection = CollectionFacade::make('test'); - $collection->cascade(['seo' => false]); - $collection->save(); - - $templatesCollection = CollectionFacade::make('structured_data_templates'); - $templatesCollection->save(); - $entry = (new Entry) - ->collection($templatesCollection) - ->id('template-123'); - - $entry->use_for_collection = $collection; - $entryMock = $entry; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { - $mock->shouldReceive('parent')->andReturn($entryMock); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getEntryFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - /** @var array> $result */ - $fieldNames = array_column($result, 'name'); - - $this->assertNotContains('seo:title', $fieldNames); - } - - #[Test] - public function get_term_fields_appends_seo_pro_variables_when_seo_pro_is_available(): void - { - $fieldtype = new class extends AvailableVariablesFieldtype - { - protected function seoProIsInstalled(): bool - { - return true; - } - }; - - /** @var Taxonomy $taxonomy */ - $taxonomy = TaxonomyFacade::make('test'); - $taxonomy->save(); - $blueprint = BlueprintFacade::make('test'); - $blueprint->setNamespace('taxonomies.test'); - $blueprint->setContents([ - 'fields' => [ - [ - 'handle' => 'title', - 'field' => [ - 'type' => 'text', - 'display' => 'Title', - ], - ], - ], - ]); - $blueprint->save(); - /** @phpstan-ignore-next-line */ - $taxonomy->termBlueprints(['test']); - - $templatesCollection = CollectionFacade::make('structured_data_templates'); - $templatesCollection->save(); - $entry = (new Entry) - ->collection($templatesCollection) - ->id('template-123'); - - $entry->use_for_taxonomy = $taxonomy; - $entryMock = $entry; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { - $mock->shouldReceive('parent')->andReturn($entryMock); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getTermFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - /** @var array> $result */ - $fieldNames = array_column($result, 'name'); - - $this->assertContains('seo:description', $fieldNames); - } - - #[Test] - public function get_term_fields_omits_seo_pro_variables_when_seo_is_disabled_for_taxonomy(): void - { - $fieldtype = new class extends AvailableVariablesFieldtype - { - protected function seoProIsInstalled(): bool - { - return true; - } - }; - - /** @var Taxonomy $taxonomy */ - $taxonomy = TaxonomyFacade::make('test'); - $taxonomy->cascade(['seo' => false]); - $taxonomy->save(); - - $templatesCollection = CollectionFacade::make('structured_data_templates'); - $templatesCollection->save(); - $entry = (new Entry) - ->collection($templatesCollection) - ->id('template-123'); - - $entry->use_for_taxonomy = $taxonomy; - $entryMock = $entry; - - /** @var Field $fieldMock */ - $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { - $mock->shouldReceive('parent')->andReturn($entryMock); - }); - - $fieldtype->setField($fieldMock); - - $reflection = new \ReflectionClass($fieldtype); - $method = $reflection->getMethod('getTermFields'); - $method->setAccessible(true); - - $result = $method->invoke($fieldtype); - - $this->assertIsArray($result); - /** @var array> $result */ - $fieldNames = array_column($result, 'name'); - - $this->assertNotContains('seo:title', $fieldNames); + $this->assertArrayHasKey('variables', $result); + /** @var array $resultVariables */ + $resultVariables = $result['variables']; + $this->assertArrayHasKey('runway', $resultVariables); } } diff --git a/tests/Unit/Parser/StructuredDataParserTest.php b/tests/Unit/Parser/StructuredDataParserTest.php index 76222ac..0194bc5 100644 --- a/tests/Unit/Parser/StructuredDataParserTest.php +++ b/tests/Unit/Parser/StructuredDataParserTest.php @@ -4,6 +4,7 @@ use Illuminate\Support\Collection as SupportCollection; use Justbetter\StatamicStructuredData\Parser\StructuredDataParser; +use Justbetter\StatamicStructuredData\Tests\Stubs\Product; use Justbetter\StatamicStructuredData\Tests\TestCase; use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; @@ -617,4 +618,75 @@ public function render_html_from_bard_segments_handles_text_value_instance(): vo $this->assertSame('

Value text

', $result); } + + #[Test] + public function resolve_absolute_url_uses_model_url_attribute(): void + { + $model = new Product([ + 'url' => '/tenways-cgo800s', + ]); + + $parser = new StructuredDataParser; + $method = (new \ReflectionClass($parser))->getMethod('resolveAbsoluteUrl'); + $method->setAccessible(true); + + $result = $method->invoke($parser, $model); + + $this->assertIsString($result); + $this->assertStringContainsString('/tenways-cgo800s', $result); + } + + #[Test] + public function resolve_absolute_url_keeps_absolute_model_urls(): void + { + $model = new Product([ + 'url' => 'https://example.com/bike', + ]); + + $parser = new StructuredDataParser; + $method = (new \ReflectionClass($parser))->getMethod('resolveAbsoluteUrl'); + $method->setAccessible(true); + + $this->assertSame('https://example.com/bike', $method->invoke($parser, $model)); + } + + #[Test] + public function resolve_absolute_url_returns_empty_string_for_model_without_url(): void + { + $model = new Product; + + $parser = new StructuredDataParser; + $method = (new \ReflectionClass($parser))->getMethod('resolveAbsoluteUrl'); + $method->setAccessible(true); + + $this->assertSame('', $method->invoke($parser, $model)); + } + + #[Test] + public function parse_context_merges_model_attributes(): void + { + $site = $this->mock(Site::class, function (MockInterface $mock): void { + $mock->shouldReceive('toAugmentedArray')->andReturn(['handle' => 'default'])->byDefault(); + $mock->shouldReceive('handle')->andReturn('default')->byDefault(); + }); + SiteFacade::shouldReceive('current')->andReturn($site)->byDefault(); + SiteFacade::shouldReceive('multiEnabled')->andReturn(false)->byDefault(); + SiteFacade::shouldReceive('hasMultiple')->andReturn(false)->byDefault(); + SiteFacade::shouldReceive('default')->andReturn($site)->byDefault(); + SiteFacade::shouldReceive('all')->andReturn(collect([$site]))->byDefault(); + + $model = new Product([ + 'name' => 'Test Bike', + 'sku' => 'BIKE-1', + ]); + + $parser = new StructuredDataParser; + $method = (new \ReflectionClass($parser))->getMethod('getParseContext'); + $method->setAccessible(true); + $result = $method->invoke($parser, $model); + + $this->assertIsArray($result); + $this->assertSame('Test Bike', $result['name']); + $this->assertSame('BIKE-1', $result['sku']); + } } diff --git a/tests/Unit/Resolvers/EntryResolverTest.php b/tests/Unit/Resolvers/EntryResolverTest.php new file mode 100644 index 0000000..e4def11 --- /dev/null +++ b/tests/Unit/Resolvers/EntryResolverTest.php @@ -0,0 +1,165 @@ +save(); + $entry = (new Entry)->collection($collection)->id('entry-123'); + + URL::shouldReceive('getCurrent')->andReturn('/test-url'); + $site = $this->mock(Site::class, function (MockInterface $mock): void { + $mock->shouldReceive('handle')->andReturn('default'); + }); + SiteFacade::shouldReceive('current')->andReturn($site); + SiteFacade::shouldReceive('multiEnabled')->andReturn(false); + EntryFacade::shouldReceive('findByUri')->with('/test-url', 'default')->andReturn($entry); + + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new EntryResolver($service); + + $result = $resolver->resolveCurrent(); + + $this->assertInstanceOf(Entry::class, $result); + } + + #[Test] + public function resolve_current_returns_page(): void + { + $page = $this->mock(Page::class); + + URL::shouldReceive('getCurrent')->andReturn('/test-url'); + $site = $this->mock(Site::class, function (MockInterface $mock): void { + $mock->shouldReceive('handle')->andReturn('default'); + }); + SiteFacade::shouldReceive('current')->andReturn($site); + SiteFacade::shouldReceive('multiEnabled')->andReturn(false); + EntryFacade::shouldReceive('findByUri')->with('/test-url', 'default')->andReturn($page); + + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new EntryResolver($service); + + $result = $resolver->resolveCurrent(); + + $this->assertInstanceOf(Page::class, $result); + } + + #[Test] + public function resolve_current_returns_null_when_not_entry_or_page(): void + { + URL::shouldReceive('getCurrent')->andReturn('/test-url'); + $site = $this->mock(Site::class, function (MockInterface $mock): void { + $mock->shouldReceive('handle')->andReturn('default'); + }); + SiteFacade::shouldReceive('current')->andReturn($site); + SiteFacade::shouldReceive('multiEnabled')->andReturn(false); + EntryFacade::shouldReceive('findByUri')->with('/test-url', 'default')->andReturn(null); + + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new EntryResolver($service); + + $result = $resolver->resolveCurrent(); + + $this->assertNull($result); + } + + #[Test] + public function supports_returns_true_for_entry_and_page(): void + { + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new EntryResolver($service); + + $this->assertTrue($resolver->supports(new Entry)); + $this->assertTrue($resolver->supports($this->mock(Page::class))); + $this->assertFalse($resolver->supports('not-an-entry')); + } + + #[Test] + public function handle_returns_null_for_unsupported_item(): void + { + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new EntryResolver($service); + + $this->assertNull($resolver->handle('not-an-entry')); + } + + #[Test] + public function handle_returns_null_when_collection_not_enabled(): void + { + Config::set('justbetter.structured-data.collections', ['other']); + + $collection = CollectionFacade::make('blog'); + $collection->save(); + $entry = (new Entry)->collection($collection)->id('entry-123'); + + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new EntryResolver($service); + + $this->assertNull($resolver->handle($entry)); + } + + #[Test] + public function handle_returns_scripts_for_enabled_entry(): void + { + Config::set('justbetter.structured-data.collections', ['blog']); + + $collection = CollectionFacade::make('blog'); + $collection->save(); + $entry = (new Entry)->collection($collection)->id('entry-123'); + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock) use ($entry): void { + $mock->shouldReceive('getJsonLdScripts')->once()->with($entry)->andReturn(['', '']); + }); + + /** @var StructuredDataService $service */ + $resolver = new EntryResolver($service); + + $this->assertSame("\n", $resolver->handle($entry)); + } + + #[Test] + public function handle_unwraps_page_to_entry(): void + { + Config::set('justbetter.structured-data.collections', ['blog']); + + $collection = CollectionFacade::make('blog'); + $collection->save(); + $entry = (new Entry)->collection($collection)->id('entry-123'); + $page = $this->mock(Page::class, function (MockInterface $mock) use ($entry): void { + $mock->shouldReceive('entry')->andReturn($entry); + }); + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock) use ($entry): void { + $mock->shouldReceive('getJsonLdScripts')->once()->with($entry)->andReturn(['']); + }); + + /** @var StructuredDataService $service */ + $resolver = new EntryResolver($service); + + $this->assertSame('', $resolver->handle($page)); + } +} diff --git a/tests/Unit/Resolvers/RunwayResolverTest.php b/tests/Unit/Resolvers/RunwayResolverTest.php new file mode 100644 index 0000000..0a3ff52 --- /dev/null +++ b/tests/Unit/Resolvers/RunwayResolverTest.php @@ -0,0 +1,160 @@ +mock(StructuredDataService::class); + $resolver = new RunwayResolver($service); + + $this->assertNull($resolver->resolveCurrent()); + } + + #[Test] + public function resolve_current_returns_null_when_uri_result_is_not_model(): void + { + RunwaySupport::fakeInstalled(true); + ModelRepository::$findByUriResult = 'not-a-model'; + + URL::shouldReceive('getCurrent')->andReturn('/product-url'); + + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new RunwayResolver($service); + + $this->assertNull($resolver->resolveCurrent()); + } + + #[Test] + public function resolve_current_returns_null_when_resource_not_enabled(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', []); + + $model = new Product; + + ModelRepository::$findByUriResult = $model; + URL::shouldReceive('getCurrent')->andReturn('/product-url'); + + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new RunwayResolver($service); + + $this->assertNull($resolver->resolveCurrent()); + } + + #[Test] + public function resolve_current_returns_model_when_enabled(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', ['product']); + + $resource = new Resource; + $resource->resourceHandle = 'product'; + + $model = new Product; + $model->runwayResource = $resource; + + ModelRepository::$findByUriResult = $model; + URL::shouldReceive('getCurrent')->andReturn('/product-url'); + + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new RunwayResolver($service); + + $this->assertSame($model, $resolver->resolveCurrent()); + } + + #[Test] + public function supports_returns_true_for_model(): void + { + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new RunwayResolver($service); + + $this->assertTrue($resolver->supports(new Product)); + $this->assertFalse($resolver->supports('not-a-model')); + } + + #[Test] + public function handle_returns_null_for_unsupported_item(): void + { + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new RunwayResolver($service); + + $this->assertNull($resolver->handle('not-a-model')); + } + + #[Test] + public function handle_returns_null_when_resource_not_enabled(): void + { + Config::set('justbetter.structured-data.runway', []); + + $model = new Product; + + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new RunwayResolver($service); + + $this->assertNull($resolver->handle($model, 'product')); + } + + #[Test] + public function handle_returns_null_when_scripts_empty(): void + { + Config::set('justbetter.structured-data.runway', ['product']); + + $model = new Product; + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock) use ($model): void { + $mock->shouldReceive('getJsonLdScripts') + ->once() + ->with($model, false, 'product') + ->andReturn([]); + }); + + /** @var StructuredDataService $service */ + $resolver = new RunwayResolver($service); + + $this->assertNull($resolver->handle($model, 'product')); + } + + #[Test] + public function handle_returns_imploded_scripts(): void + { + Config::set('justbetter.structured-data.runway', ['product']); + + $model = new Product; + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock) use ($model): void { + $mock->shouldReceive('getJsonLdScripts') + ->once() + ->with($model, false, 'product') + ->andReturn(['', '']); + }); + + /** @var StructuredDataService $service */ + $resolver = new RunwayResolver($service); + + $this->assertSame("\n", $resolver->handle($model, 'product')); + } +} diff --git a/tests/Unit/Resolvers/TaxonomyResolverTest.php b/tests/Unit/Resolvers/TaxonomyResolverTest.php new file mode 100644 index 0000000..323fe08 --- /dev/null +++ b/tests/Unit/Resolvers/TaxonomyResolverTest.php @@ -0,0 +1,129 @@ +save(); + $term = $this->mock(LocalizedTerm::class, function (MockInterface $mock) use ($taxonomy): void { + $mock->shouldReceive('taxonomy')->andReturn($taxonomy); + }); + + URL::shouldReceive('getCurrent')->andReturn('/test-url'); + $site = $this->mock(Site::class, function (MockInterface $mock): void { + $mock->shouldReceive('handle')->andReturn('default'); + }); + SiteFacade::shouldReceive('current')->andReturn($site); + SiteFacade::shouldReceive('multiEnabled')->andReturn(false); + Term::shouldReceive('findByUri')->with('/test-url', 'default')->andReturn($term); + + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new TaxonomyResolver($service); + + $result = $resolver->resolveCurrent(); + + $this->assertInstanceOf(LocalizedTerm::class, $result); + } + + #[Test] + public function resolve_current_returns_null_when_not_localized_term(): void + { + URL::shouldReceive('getCurrent')->andReturn('/test-url'); + $site = $this->mock(Site::class, function (MockInterface $mock): void { + $mock->shouldReceive('handle')->andReturn('default'); + }); + SiteFacade::shouldReceive('current')->andReturn($site); + SiteFacade::shouldReceive('multiEnabled')->andReturn(false); + Term::shouldReceive('findByUri')->with('/test-url', 'default')->andReturn(null); + + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new TaxonomyResolver($service); + + $result = $resolver->resolveCurrent(); + + $this->assertNull($result); + } + + #[Test] + public function supports_returns_true_for_localized_term(): void + { + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new TaxonomyResolver($service); + + $this->assertTrue($resolver->supports($this->mock(LocalizedTerm::class))); + $this->assertFalse($resolver->supports('not-a-term')); + } + + #[Test] + public function handle_returns_null_for_unsupported_item(): void + { + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new TaxonomyResolver($service); + + $this->assertNull($resolver->handle('not-a-term')); + } + + #[Test] + public function handle_returns_null_when_taxonomy_not_enabled(): void + { + Config::set('justbetter.structured-data.taxonomies', ['other']); + + /** @var Taxonomy $taxonomy */ + $taxonomy = TaxonomyFacade::make('categories'); + $taxonomy->save(); + $term = $this->mock(LocalizedTerm::class, function (MockInterface $mock) use ($taxonomy): void { + $mock->shouldReceive('taxonomy')->andReturn($taxonomy); + }); + + /** @var StructuredDataService $service */ + $service = $this->mock(StructuredDataService::class); + $resolver = new TaxonomyResolver($service); + + $this->assertNull($resolver->handle($term)); + } + + #[Test] + public function handle_returns_scripts_for_enabled_term(): void + { + Config::set('justbetter.structured-data.taxonomies', ['categories']); + + /** @var Taxonomy $taxonomy */ + $taxonomy = TaxonomyFacade::make('categories'); + $taxonomy->save(); + $term = $this->mock(LocalizedTerm::class, function (MockInterface $mock) use ($taxonomy): void { + $mock->shouldReceive('taxonomy')->andReturn($taxonomy); + }); + + $service = $this->mock(StructuredDataService::class, function (MockInterface $mock) use ($term): void { + $mock->shouldReceive('getJsonLdScripts')->once()->with($term)->andReturn(['']); + }); + + /** @var StructuredDataService $service */ + $resolver = new TaxonomyResolver($service); + + $this->assertSame('', $resolver->handle($term)); + } +} diff --git a/tests/Unit/Services/AvailableVariables/AvailableVariablesProvidersTest.php b/tests/Unit/Services/AvailableVariables/AvailableVariablesProvidersTest.php new file mode 100644 index 0000000..2a8581e --- /dev/null +++ b/tests/Unit/Services/AvailableVariables/AvailableVariablesProvidersTest.php @@ -0,0 +1,609 @@ +save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $this->assertSame([], $service->entry()->variables(null)); + $this->assertSame([], $service->entry()->variables($entry)); + } + + #[Test] + public function get_entry_fields_handles_no_blueprints_gracefully(): void + { + $service = new AvailableVariablesService; + + $collection = CollectionFacade::make('test'); + $collection->save(); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $collectionMock = Mockery::mock($collection)->makePartial(); + $collectionMock->shouldReceive('entryBlueprints')->andReturn(collect([])); + + $entry->use_for_collection = $collectionMock; + + $result = $service->entry()->variables($entry); + + $this->assertNotEmpty($result); + } + + #[Test] + public function get_entry_fields_includes_base_fields_and_maps_collection_fields(): void + { + $service = new AvailableVariablesService; + + $collection = CollectionFacade::make('test'); + $collection->save(); + $blueprint = BlueprintFacade::make('test'); + $blueprint->setNamespace('collections.test'); + $blueprint->setContents([ + 'fields' => [ + [ + 'handle' => 'title', + 'field' => [ + 'type' => 'text', + 'display' => 'Title', + ], + ], + ], + ]); + $blueprint->save(); + $collection->entryBlueprints(); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_collection = $collection; + + $result = $service->entry()->variables($entry); + + $this->assertNotEmpty($result); + /** @var array $firstResult */ + $firstResult = $result[0]; + $this->assertEquals('absolute_url', $firstResult['name']); + $this->assertGreaterThanOrEqual(2, count($result)); + $fieldNames = array_column($result, 'name'); + $this->assertContains('title', $fieldNames); + } + + #[Test] + public function get_entry_fields_handles_empty_fields_after_merge_gracefully(): void + { + $service = new AvailableVariablesService; + + $collection = CollectionFacade::make('test'); + $collection->save(); + $blueprint = $this->mock(Blueprint::class); + $fieldsMock = $this->mock(Fields::class, function (MockInterface $mock): void { + $mock->shouldReceive('items')->andReturn([]); + }); + $blueprint->shouldReceive('fields')->andReturn($fieldsMock); + + $collectionMock = Mockery::mock($collection)->makePartial(); + $collectionMock->shouldReceive('entryBlueprints')->andReturn(collect([$blueprint])); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_collection = $collectionMock; + + $result = $service->entry()->variables($entry); + + $this->assertNotEmpty($result); + } + + #[Test] + public function get_entry_fields_appends_seo_pro_variables_when_seo_pro_is_available(): void + { + $seoProVariables = new InstalledSeoProVariables; + + $provider = new EntryVariableProvider(new BlueprintVariableMapper, $seoProVariables); + + $collection = CollectionFacade::make('test'); + $collection->save(); + $blueprint = BlueprintFacade::make('test'); + $blueprint->setNamespace('collections.test'); + $blueprint->setContents([ + 'fields' => [ + [ + 'handle' => 'title', + 'field' => [ + 'type' => 'text', + 'display' => 'Title', + ], + ], + ], + ]); + $blueprint->save(); + $collection->entryBlueprints(); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_collection = $collection; + + $result = $provider->variables($entry); + + $fieldNames = array_column($result, 'name'); + + $this->assertContains('seo:compiled_title', $fieldNames); + + $titleIndex = array_search('title', $fieldNames, true); + $seoTitleIndex = array_search('seo:title', $fieldNames, true); + + $this->assertNotFalse($titleIndex); + $this->assertNotFalse($seoTitleIndex); + $this->assertGreaterThan($titleIndex, $seoTitleIndex); + } + + #[Test] + public function get_entry_fields_omits_seo_pro_variables_when_seo_is_disabled_for_collection(): void + { + $seoProVariables = new InstalledSeoProVariables; + + $provider = new EntryVariableProvider(new BlueprintVariableMapper, $seoProVariables); + + $collection = CollectionFacade::make('test'); + $collection->cascade(['seo' => false]); + $collection->save(); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_collection = $collection; + + $result = $provider->variables($entry); + + $fieldNames = array_column($result, 'name'); + + $this->assertNotContains('seo:title', $fieldNames); + } + + #[Test] + public function get_term_fields_returns_empty_when_no_taxonomy(): void + { + $service = new AvailableVariablesService; + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $this->assertSame([], $service->term()->variables(null)); + $this->assertSame([], $service->term()->variables($entry)); + } + + #[Test] + public function get_term_fields_handles_no_blueprints_gracefully(): void + { + $service = new AvailableVariablesService; + + /** @var Taxonomy $taxonomy */ + $taxonomy = TaxonomyFacade::make('test'); + $taxonomy->save(); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $taxonomyMock = Mockery::mock($taxonomy)->makePartial(); + $taxonomyMock->shouldReceive('termBlueprints')->andReturn(collect([])); + + $entry->use_for_taxonomy = $taxonomyMock; + + $result = $service->term()->variables($entry); + + $this->assertNotEmpty($result); + } + + #[Test] + public function get_term_fields_handles_null_first_blueprint_gracefully(): void + { + $service = new AvailableVariablesService; + + /** @var Taxonomy $taxonomy */ + $taxonomy = TaxonomyFacade::make('test'); + $taxonomy->save(); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $taxonomyMock = Mockery::mock($taxonomy)->makePartial(); + $taxonomyMock->shouldReceive('termBlueprints')->andReturn(collect([null])); + + $entry->use_for_taxonomy = $taxonomyMock; + + $result = $service->term()->variables($entry); + + $this->assertNotEmpty($result); + } + + #[Test] + public function get_term_fields_handles_empty_fields_after_merge_gracefully(): void + { + $service = new AvailableVariablesService; + + /** @var Taxonomy $taxonomy */ + $taxonomy = TaxonomyFacade::make('test'); + $taxonomy->save(); + $blueprint = $this->mock(Blueprint::class); + $fieldsMock = $this->mock(Fields::class, function (MockInterface $mock): void { + $mock->shouldReceive('items')->andReturn([]); + }); + $blueprint->shouldReceive('fields')->andReturn($fieldsMock); + + $taxonomyMock = Mockery::mock($taxonomy)->makePartial(); + $taxonomyMock->shouldReceive('termBlueprints')->andReturn(collect([$blueprint])); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_taxonomy = $taxonomyMock; + + $result = $service->term()->variables($entry); + + $this->assertNotEmpty($result); + } + + #[Test] + public function get_term_fields_includes_base_fields_and_maps_taxonomy_fields(): void + { + $service = new AvailableVariablesService; + + /** @var Taxonomy $taxonomy */ + $taxonomy = TaxonomyFacade::make('test'); + $taxonomy->save(); + $blueprint = BlueprintFacade::make('test'); + $blueprint->setNamespace('taxonomies.test'); + $blueprint->setContents([ + 'fields' => [ + [ + 'handle' => 'title', + 'field' => [ + 'type' => 'text', + 'display' => 'Title', + ], + ], + ], + ]); + $blueprint->save(); + /** @phpstan-ignore-next-line */ + $taxonomy->termBlueprints(['test']); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_taxonomy = $taxonomy; + + $result = $service->term()->variables($entry); + + $this->assertNotEmpty($result); + /** @var array $firstResult */ + $firstResult = $result[0]; + $this->assertEquals('absolute_url', $firstResult['name']); + $this->assertGreaterThanOrEqual(2, count($result)); + $fieldNames = array_column($result, 'name'); + $this->assertContains('title', $fieldNames); + } + + #[Test] + public function get_term_fields_appends_seo_pro_variables_when_seo_pro_is_available(): void + { + $seoProVariables = new InstalledSeoProVariables; + + $provider = new TermVariableProvider(new BlueprintVariableMapper, $seoProVariables); + + /** @var Taxonomy $taxonomy */ + $taxonomy = TaxonomyFacade::make('test'); + $taxonomy->save(); + $blueprint = BlueprintFacade::make('test'); + $blueprint->setNamespace('taxonomies.test'); + $blueprint->setContents([ + 'fields' => [ + [ + 'handle' => 'title', + 'field' => [ + 'type' => 'text', + 'display' => 'Title', + ], + ], + ], + ]); + $blueprint->save(); + /** @phpstan-ignore-next-line */ + $taxonomy->termBlueprints(['test']); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_taxonomy = $taxonomy; + + $result = $provider->variables($entry); + + $fieldNames = array_column($result, 'name'); + + $this->assertContains('seo:description', $fieldNames); + } + + #[Test] + public function get_term_fields_omits_seo_pro_variables_when_seo_is_disabled_for_taxonomy(): void + { + $seoProVariables = new InstalledSeoProVariables; + + $provider = new TermVariableProvider(new BlueprintVariableMapper, $seoProVariables); + + /** @var Taxonomy $taxonomy */ + $taxonomy = TaxonomyFacade::make('test'); + $taxonomy->cascade(['seo' => false]); + $taxonomy->save(); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_taxonomy = $taxonomy; + + $result = $provider->variables($entry); + + $fieldNames = array_column($result, 'name'); + + $this->assertNotContains('seo:title', $fieldNames); + } + + #[Test] + public function get_global_variables_returns_variables(): void + { + $service = new AvailableVariablesService; + + $blueprint = BlueprintFacade::make('test'); + $blueprint->setContents([ + 'fields' => [ + [ + 'handle' => 'test_field', + 'field' => [ + 'type' => 'text', + 'display' => 'Test Field', + ], + ], + ], + ]); + $blueprint->save(); + + $globalSet = GlobalSetFacade::make('test'); + $globalSet->save(); + $globalSet->blueprint('test'); + + $fieldsMock = $this->mock(Fields::class, function (MockInterface $mock): void { + $mock->shouldReceive('items')->andReturn(collect([ + [ + 'handle' => 'test_field', + 'field' => [ + 'type' => 'text', + 'display' => 'Test Field', + ], + ], + ])); + }); + + $blueprintMock = $this->mock(Blueprint::class, function (MockInterface $mock) use ($fieldsMock): void { + $mock->shouldReceive('fields')->andReturn($fieldsMock); + }); + + $globalSetMock = Mockery::mock($globalSet)->makePartial(); + $globalSetMock->shouldReceive('blueprint')->andReturn($blueprintMock); + $globalSetMock->shouldReceive('handle')->andReturn('test'); + + $globalCollection = new GlobalCollection([$globalSetMock]); + GlobalSet::shouldReceive('all')->andReturn($globalCollection); + + $result = $service->globals()->variables(); + + $this->assertNotEmpty($result); + } + + #[Test] + public function get_seo_pro_variables_returns_core_cascaded_fields(): void + { + $service = new AvailableVariablesService; + + $result = $service->seoPro()->all(); + + $this->assertCount(7, $result); + $names = array_column($result, 'name'); + $this->assertSame([ + 'seo:title', + 'seo:compiled_title', + 'seo:description', + 'seo:canonical_url', + 'seo:og_title', + 'seo:og_type', + 'seo:image', + ], $names); + } + + #[Test] + public function get_runway_fields_returns_empty_when_runway_not_installed(): void + { + RunwaySupport::fakeInstalled(false); + + $service = new AvailableVariablesService; + + $this->assertSame([], $service->runway()->variables(null)); + } + + #[Test] + public function get_runway_fields_returns_empty_when_parent_is_not_entry(): void + { + RunwaySupport::fakeInstalled(true); + + $service = new AvailableVariablesService; + + $this->assertSame([], $service->runway()->variables(null)); + } + + #[Test] + public function get_runway_fields_returns_empty_when_no_resource_handle(): void + { + RunwaySupport::fakeInstalled(true); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry)->collection($templatesCollection)->id('template-123'); + + $service = new AvailableVariablesService; + + $this->assertSame([], $service->runway()->variables($entry)); + } + + #[Test] + public function get_runway_fields_returns_empty_when_resource_not_found(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', ['product']); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry)->collection($templatesCollection)->id('template-123'); + $entry->use_for_runway = 'product'; + + $service = new AvailableVariablesService; + + $this->assertSame([], $service->runway()->variables($entry)); + } + + #[Test] + public function get_runway_fields_returns_blueprint_and_model_variables(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', ['product']); + + $blueprint = BlueprintFacade::make('runway-product')->setContents([ + 'fields' => [ + [ + 'handle' => 'name', + 'field' => [ + 'type' => 'text', + 'display' => 'Name', + ], + ], + ], + ]); + + $model = new Product([ + 'sku' => 'ABC', + 'name' => 'Bike', + ]); + $model->fillable(['sku', 'color']); + + $resource = new Resource; + $resource->resourceHandle = 'product'; + $resource->resourceName = 'Products'; + $resource->resourceBlueprint = $blueprint; + $resource->resourceModel = $model; + Runway::$findResults['product'] = $resource; + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry)->collection($templatesCollection)->id('template-123'); + $entry->use_for_runway = 'product'; + + $service = new AvailableVariablesService; + + $result = $service->runway()->variables($entry); + + $names = array_column($result, 'name'); + $this->assertContains('absolute_url', $names); + $this->assertContains('name', $names); + $this->assertContains('sku', $names); + $this->assertContains('color', $names); + } + + #[Test] + public function resolve_runway_resource_handle_unwraps_labeled_value(): void + { + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + + $entry = new class extends Entry + { + public mixed $use_for_runway; + }; + $entry->collection($templatesCollection)->id('template-123'); + $entry->use_for_runway = new LabeledValue('product', 'Products'); + + $service = new AvailableVariablesService; + + $this->assertSame('product', $service->runway()->resolveResourceHandle($entry)); + } +} diff --git a/tests/Unit/Services/AvailableVariables/BlueprintVariableMapperTest.php b/tests/Unit/Services/AvailableVariables/BlueprintVariableMapperTest.php new file mode 100644 index 0000000..864680a --- /dev/null +++ b/tests/Unit/Services/AvailableVariables/BlueprintVariableMapperTest.php @@ -0,0 +1,560 @@ +assertTrue($mapper->fieldTypeIsEligible('text')); + $this->assertTrue($mapper->fieldTypeIsEligible('assets')); + $this->assertTrue($mapper->fieldTypeIsEligible('bard')); + $this->assertTrue($mapper->fieldTypeIsEligible('toggle')); + $this->assertTrue($mapper->fieldTypeIsEligible('integer')); + $this->assertTrue($mapper->fieldTypeIsEligible('slug')); + $this->assertTrue($mapper->fieldTypeIsEligible('date')); + $this->assertTrue($mapper->fieldTypeIsEligible('entries')); + $this->assertTrue($mapper->fieldTypeIsEligible('aardvark_seo_meta_title')); + $this->assertTrue($mapper->fieldTypeIsEligible('aardvark_seo_meta_description')); + } + + #[Test] + public function field_type_is_eligible_returns_false_for_ineligible_types(): void + { + $mapper = new BlueprintVariableMapper; + + $this->assertFalse($mapper->fieldTypeIsEligible('unknown_type')); + $this->assertFalse($mapper->fieldTypeIsEligible('grid')); + $this->assertFalse($mapper->fieldTypeIsEligible('replicator')); + } + + #[Test] + public function set_field_data_returns_null_for_ineligible_field(): void + { + $mapper = new BlueprintVariableMapper; + + $field = [ + 'handle' => 'test', + 'field' => [ + 'type' => 'grid', + ], + ]; + + $result = $mapper->setFieldData($field); + + $this->assertNull($result); + } + + #[Test] + public function set_field_data_returns_null_when_handle_is_parent(): void + { + $mapper = new BlueprintVariableMapper; + + $field = [ + 'handle' => 'parent', + 'field' => [ + 'type' => 'text', + ], + ]; + + $result = $mapper->setFieldData($field); + + $this->assertNull($result); + } + + #[Test] + public function set_field_data_returns_field_data_for_eligible_field(): void + { + $mapper = new BlueprintVariableMapper; + + $field = [ + 'handle' => 'test_field', + 'field' => [ + 'type' => 'text', + 'display' => 'Test Field', + ], + ]; + + $result = $mapper->setFieldData($field); + + $this->assertIsArray($result); + /** @var array $result */ + $this->assertArrayHasKey('name', $result); + $this->assertArrayHasKey('description', $result); + $this->assertArrayHasKey('children', $result); + $this->assertEquals('test_field', $result['name']); + } + + #[Test] + public function get_collection_variables_returns_empty_when_no_collection_handle(): void + { + $mapper = new BlueprintVariableMapper; + + $result = $mapper->getCollectionVariables('', []); + + $this->assertEmpty($result); + } + + #[Test] + public function get_collection_variables_returns_empty_when_handle_is_structured_data_templates(): void + { + $mapper = new BlueprintVariableMapper; + + $result = $mapper->getCollectionVariables('structured_data_templates', []); + + $this->assertEmpty($result); + } + + #[Test] + public function get_collection_variables_returns_empty_when_collection_not_found(): void + { + $mapper = new BlueprintVariableMapper; + + CollectionFacade::shouldReceive('find')->with('nonexistent')->andReturn(null); + + $result = $mapper->getCollectionVariables('nonexistent', []); + + $this->assertEmpty($result); + } + + #[Test] + public function get_collection_variables_returns_variables_when_collection_exists(): void + { + $mapper = new BlueprintVariableMapper; + + $collection = CollectionFacade::make('blog'); + $collection->save(); + $blueprint = BlueprintFacade::make('blog'); + $blueprint->setContents([ + 'fields' => [ + [ + 'handle' => 'title', + 'field' => [ + 'type' => 'text', + 'display' => 'Title', + ], + ], + ], + ]); + $blueprint->save(); + $collection->entryBlueprints(); + + $field = [ + 'handle' => 'related_entry', + 'field' => [ + 'type' => 'entries', + 'display' => 'Related Entry', + 'collections' => ['blog'], + ], + ]; + + CollectionFacade::shouldReceive('find')->with('blog')->andReturn($collection); + + $result = $mapper->getCollectionVariables('blog', $field); + + $this->assertNotEmpty($result); + } + + #[Test] + public function get_collection_variables_returns_empty_when_no_blueprint(): void + { + $mapper = new BlueprintVariableMapper; + + $collectionMock = $this->mock(Collection::class, function ($mock): void { + $mock->shouldReceive('entryBlueprints')->andReturn(collect([])); + }); + + $field = [ + 'handle' => 'related_entry', + 'field' => [ + 'type' => 'entries', + 'display' => 'Related Entry', + 'collections' => ['blog'], + ], + ]; + + CollectionFacade::shouldReceive('find')->with('blog')->andReturn($collectionMock); + + $result = $mapper->getCollectionVariables('blog', $field); + + $this->assertEmpty($result); + } + + #[Test] + public function set_field_data_returns_field_with_children_for_entries_field_type(): void + { + $mapper = new BlueprintVariableMapper; + + $collection = CollectionFacade::make('blog'); + $collection->save(); + $blueprint = BlueprintFacade::make('blog'); + $blueprint->setContents([ + 'fields' => [ + [ + 'handle' => 'title', + 'field' => [ + 'type' => 'text', + 'display' => 'Title', + ], + ], + ], + ]); + $blueprint->save(); + $collection->entryBlueprints(); + + CollectionFacade::shouldReceive('find')->with('blog')->andReturn($collection); + + $field = [ + 'handle' => 'related_entry', + 'field' => [ + 'type' => 'entries', + 'display' => 'Related Entry', + 'collections' => ['blog'], + ], + ]; + + $result = $mapper->setFieldData($field); + + $this->assertIsArray($result); + /** @var array $result */ + $this->assertArrayHasKey('name', $result); + $this->assertArrayHasKey('description', $result); + $this->assertArrayHasKey('children', $result); + $this->assertNotEmpty($result['children']); + } + + #[Test] + public function set_field_data_returns_null_when_entries_field_has_no_collections(): void + { + $mapper = new BlueprintVariableMapper; + + $field = [ + 'handle' => 'related_entry', + 'field' => [ + 'type' => 'entries', + 'display' => 'Related Entry', + ], + ]; + + $result = $mapper->setFieldData($field); + + $this->assertNull($result); + } + + #[Test] + public function set_field_data_returns_group_field_with_supported_children(): void + { + $mapper = new BlueprintVariableMapper; + + $field = [ + 'handle' => 'media_options', + 'field' => [ + 'type' => 'group', + 'display' => 'Media opties', + 'fields' => [ + [ + 'handle' => 'object_fit', + 'field' => [ + 'type' => 'select', + 'display' => 'Object fit', + ], + ], + [ + 'handle' => 'video', + 'field' => [ + 'type' => 'toggle', + 'display' => 'Video', + ], + ], + [ + 'handle' => 'layout', + 'field' => [ + 'type' => 'grid', + 'display' => 'Layout', + ], + ], + ], + ], + ]; + + $result = $mapper->setFieldData($field); + + $this->assertIsArray($result); + /** @var array $result */ + $this->assertSame('media_options', $result['name']); + $this->assertSame('Media opties', $result['description']); + $this->assertArrayHasKey('children', $result); + $this->assertIsArray($result['children']); + $this->assertCount(2, $result['children']); + + /** @var array> $children */ + $children = $result['children']; + $childNames = array_column($children, 'name'); + $childDescriptions = array_column($children, 'description'); + + $this->assertContains('media_options:object_fit', $childNames); + $this->assertContains('Media opties: Object fit', $childDescriptions); + $this->assertContains('media_options:video', $childNames); + $this->assertContains('Media opties: Video', $childDescriptions); + $this->assertNotContains('media_options:layout', $childNames); + } + + #[Test] + public function set_field_data_returns_null_for_group_without_supported_children(): void + { + $mapper = new BlueprintVariableMapper; + + $field = [ + 'handle' => 'media_options', + 'field' => [ + 'type' => 'group', + 'display' => 'Media opties', + 'fields' => [ + [ + 'handle' => 'layout', + 'field' => [ + 'type' => 'grid', + 'display' => 'Layout', + ], + ], + ], + ], + ]; + + $result = $mapper->setFieldData($field); + + $this->assertNull($result); + } + + #[Test] + public function set_field_data_group_skips_children_without_handle(): void + { + $mapper = new BlueprintVariableMapper; + + $field = [ + 'handle' => 'media_options', + 'field' => [ + 'type' => 'group', + 'display' => 'Media opties', + 'fields' => [ + [ + 'field' => [ + 'type' => 'toggle', + 'display' => 'Missing Handle', + ], + ], + [ + 'handle' => 'video', + 'field' => [ + 'type' => 'toggle', + 'display' => 'Video', + ], + ], + ], + ], + ]; + + $result = $mapper->setFieldData($field); + + $this->assertIsArray($result); + /** @var array $result */ + $this->assertArrayHasKey('children', $result); + /** @var array> $children */ + $children = $result['children']; + $this->assertCount(1, $children); + $this->assertSame('media_options:video', $children[0]['name'] ?? null); + } + + #[Test] + public function normalize_blueprint_items_converts_collection_to_array(): void + { + $mapper = new BlueprintVariableMapper; + + $collection = collect([ + ['handle' => 'test', 'field' => ['type' => 'text']], + ]); + + $result = $mapper->normalizeBlueprintItems($collection); + + $this->assertCount(1, $result); + /** @var array $firstResult */ + $firstResult = $result[0]; + $this->assertEquals('test', $firstResult['handle']); + } + + #[Test] + public function normalize_blueprint_items_returns_array_as_is(): void + { + $mapper = new BlueprintVariableMapper; + + $input = [ + ['handle' => 'test', 'field' => ['type' => 'text']], + ]; + + $result = $mapper->normalizeBlueprintItems($input); + + $this->assertEquals($input, $result); + } + + #[Test] + public function normalize_blueprint_items_returns_empty_array_for_invalid_input(): void + { + $mapper = new BlueprintVariableMapper; + + $result = $mapper->normalizeBlueprintItems('not-an-array'); + + $this->assertEmpty($result); + } + + #[Test] + public function normalize_blueprint_items_includes_imported_fieldset_items(): void + { + $mapper = new BlueprintVariableMapper; + + $fieldsetFields = $this->mock(Fields::class, function (MockInterface $mock): void { + $mock->shouldReceive('items')->andReturn([ + [ + 'handle' => 'imported_title', + 'field' => [ + 'type' => 'text', + 'display' => 'Imported Title', + ], + ], + ]); + }); + + $fieldset = $this->mock(Fieldset::class, function (MockInterface $mock) use ($fieldsetFields): void { + $mock->shouldReceive('fields')->andReturn($fieldsetFields); + }); + + FieldsetFacade::shouldReceive('find')->with('seo_fields')->andReturn($fieldset); + + $result = $mapper->normalizeBlueprintItems([ + ['import' => 'seo_fields'], + ]); + + $this->assertCount(1, $result); + /** @var array $firstResult */ + $firstResult = $result[0]; + $this->assertSame('imported_title', $firstResult['handle']); + } + + #[Test] + public function normalize_blueprint_items_handles_missing_imported_fieldset(): void + { + $mapper = new BlueprintVariableMapper; + + FieldsetFacade::shouldReceive('find')->with('missing_fields')->andReturn(null); + + $result = $mapper->normalizeBlueprintItems([ + ['import' => 'missing_fields'], + ]); + + $this->assertEmpty($result); + } + + #[Test] + public function normalize_blueprint_items_applies_prefix_to_imported_fieldset_items(): void + { + $mapper = new BlueprintVariableMapper; + + $fieldsetFields = $this->mock(Fields::class, function (MockInterface $mock): void { + $mock->shouldReceive('items')->andReturn([ + [ + 'handle' => 'image', + 'field' => [ + 'type' => 'assets', + 'display' => 'Image', + ], + ], + ]); + }); + + $fieldset = $this->mock(Fieldset::class, function (MockInterface $mock) use ($fieldsetFields): void { + $mock->shouldReceive('fields')->andReturn($fieldsetFields); + }); + + FieldsetFacade::shouldReceive('find')->with('media')->andReturn($fieldset); + + $result = $mapper->normalizeBlueprintItems([ + ['import' => 'media', 'prefix' => 'featured_'], + ]); + + $this->assertCount(1, $result); + /** @var array $firstResult */ + $firstResult = $result[0]; + $this->assertSame('featured_image', $firstResult['handle']); + } + + #[Test] + public function normalize_blueprint_items_handles_nested_support_collection_and_invalid_items(): void + { + $mapper = new BlueprintVariableMapper; + + $nestedFields = collect([ + [ + 'handle' => 'nested_title', + 'field' => [ + 'type' => 'text', + 'display' => 'Nested Title', + ], + ], + ]); + + $items = [ + collect([ + 'handle' => 'wrapped_field', + 'field' => [ + 'type' => 'text', + 'display' => 'Wrapped Field', + ], + ]), + 'invalid-item', + [ + 'fields' => $nestedFields, + ], + ]; + + $result = $mapper->normalizeBlueprintItems($items); + + $fieldHandles = array_column($result, 'handle'); + $this->assertContains('wrapped_field', $fieldHandles); + $this->assertContains('nested_title', $fieldHandles); + } + + #[Test] + public function normalize_blueprint_items_handles_invalid_import_handle(): void + { + $mapper = new BlueprintVariableMapper; + + $result = $mapper->normalizeBlueprintItems([ + ['import' => null], + ]); + + $this->assertEmpty($result); + } + + #[Test] + public function get_imported_fieldset_items_returns_empty_for_invalid_import_config(): void + { + $mapper = new BlueprintVariableMapper; + + $result = $mapper->getImportedFieldsetItems(['import' => null]); + + $this->assertEmpty($result); + } +} diff --git a/tests/Unit/Services/ReplicatorFieldServiceTest.php b/tests/Unit/Services/ReplicatorFieldServiceTest.php index 1a0e1e3..67a0c8f 100644 --- a/tests/Unit/Services/ReplicatorFieldServiceTest.php +++ b/tests/Unit/Services/ReplicatorFieldServiceTest.php @@ -2,7 +2,9 @@ namespace Justbetter\StatamicStructuredData\Tests\Unit\Services; +use Illuminate\Support\Facades\Config; use Justbetter\StatamicStructuredData\Services\ReplicatorFieldService; +use Justbetter\StatamicStructuredData\Support\RunwaySupport; use Justbetter\StatamicStructuredData\Tests\TestCase; use Mockery; use Mockery\MockInterface; @@ -14,7 +16,10 @@ use Statamic\Facades\Taxonomy as TaxonomyFacade; use Statamic\Fields\Blueprint; use Statamic\Fields\Fields; +use Statamic\Fields\LabeledValue; use Statamic\Taxonomies\Taxonomy; +use StatamicRadPack\Runway\Resource; +use StatamicRadPack\Runway\Runway; class ReplicatorFieldServiceTest extends TestCase { @@ -1048,4 +1053,107 @@ public function it_returns_empty_array_when_taxonomy_term_blueprints_returns_emp $this->assertEmpty($result); } + + #[Test] + public function get_replicator_fields_uses_runway_blueprint_when_configured(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', ['product']); + + $blueprint = BlueprintFacade::make('runway-product')->setContents([ + 'fields' => [ + [ + 'handle' => 'blocks', + 'field' => [ + 'type' => 'replicator', + 'display' => 'Blocks', + 'sets' => [ + 'text' => [ + 'display' => 'Text', + 'fields' => [ + [ + 'handle' => 'content', + 'field' => [ + 'type' => 'text', + 'display' => 'Content', + ], + ], + ], + ], + ], + ], + ], + ], + ]); + + $resource = new Resource; + $resource->resourceHandle = 'product'; + $resource->resourceBlueprint = $blueprint; + Runway::$findResults['product'] = $resource; + + $collection = CollectionFacade::make('structured_data_templates'); + $collection->save(); + $template = (new Entry) + ->collection($collection) + ->id('template-runway'); + $template->use_for_runway = 'product'; + + $service = new ReplicatorFieldService; + $result = $service->getReplicatorFields($template); + + $this->assertNotEmpty($result); + $this->assertSame('blocks', $result[0]['handle']); + } + + #[Test] + public function get_replicator_fields_unwraps_labeled_value_runway_handle(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', ['product']); + + $blueprint = BlueprintFacade::make('runway-product-labeled')->setContents([ + 'fields' => [ + [ + 'handle' => 'blocks', + 'field' => [ + 'type' => 'replicator', + 'display' => 'Blocks', + 'sets' => [ + 'text' => [ + 'display' => 'Text', + 'fields' => [ + [ + 'handle' => 'content', + 'field' => [ + 'type' => 'text', + 'display' => 'Content', + ], + ], + ], + ], + ], + ], + ], + ], + ]); + + $resource = new Resource; + $resource->resourceHandle = 'product'; + $resource->resourceBlueprint = $blueprint; + Runway::$findResults['product'] = $resource; + + $collection = CollectionFacade::make('structured_data_templates'); + $collection->save(); + $template = new class extends Entry + { + public mixed $use_for_runway; + }; + $template->collection($collection)->id('template-runway-labeled'); + $template->use_for_runway = new LabeledValue('product', 'Products'); + + $service = new ReplicatorFieldService; + $result = $service->getReplicatorFields($template); + + $this->assertNotEmpty($result); + } } diff --git a/tests/Unit/Services/StructuredDataServiceRunwayTest.php b/tests/Unit/Services/StructuredDataServiceRunwayTest.php new file mode 100644 index 0000000..29c2bdd --- /dev/null +++ b/tests/Unit/Services/StructuredDataServiceRunwayTest.php @@ -0,0 +1,124 @@ +save(); + + $template = (new Entry) + ->collection($templatesCollection) + ->id('runway-template-1') + ->set('blueprint_type', 'runway') + ->set('use_for_runway', 'product') + ->set('schema_data', [[ + 'specialProps' => ['context' => 'https://schema.org', 'type' => 'Product'], + 'fields' => [], + ]]); + $template->save(); + + $model = new Product([ + 'name' => 'Test Bike', + 'sku' => 'BIKE-1', + ]); + + $parser = $this->mock(StructuredDataParser::class); + /** @var StructuredDataParser $parser */ + $service = new StructuredDataService($parser); + + $templates = $service->getTemplates($model, 'product'); + + $this->assertContains('runway-template-1', $templates); + } + + #[Test] + public function get_templates_returns_empty_when_runway_handle_not_enabled(): void + { + Config::set('justbetter.structured-data.runway', []); + + $model = new Product; + + $parser = $this->mock(StructuredDataParser::class); + /** @var StructuredDataParser $parser */ + $service = new StructuredDataService($parser); + + $this->assertSame([], $service->getTemplates($model, 'product')); + } + + #[Test] + public function get_json_ld_scripts_parses_schemas_for_runway_model(): void + { + Config::set('justbetter.structured-data.runway', ['product']); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + + $schema = [[ + 'specialProps' => ['context' => 'https://schema.org', 'type' => 'Product'], + 'fields' => [ + ['key' => 'name', 'type' => 'string', 'value' => '{{ name }}'], + ], + ]]; + + $template = (new Entry) + ->collection($templatesCollection) + ->id('runway-template-2') + ->set('blueprint_type', 'runway') + ->set('use_for_runway', 'product') + ->set('schema_data', $schema); + $template->save(); + + $model = new Product([ + 'name' => 'Test Bike', + ]); + + $parser = $this->mock(StructuredDataParser::class, function (MockInterface $mock) use ($schema, $model): void { + $mock->shouldReceive('parse') + ->once() + ->with($schema, $model) + ->andReturn([[ + 'specialProps' => ['context' => 'https://schema.org', 'type' => 'Product'], + 'fields' => [ + ['key' => 'name', 'type' => 'string', 'value' => 'Test Bike'], + ], + ]]); + }); + + /** @var StructuredDataParser $parser */ + $service = new StructuredDataService($parser); + + $scripts = $service->getJsonLdScripts($model, false, 'product'); + + $this->assertCount(1, $scripts); + $this->assertStringContainsString('application/ld+json', $scripts[0]); + $this->assertStringContainsString('Product', $scripts[0]); + } + + #[Test] + public function get_runway_template_ids_returns_empty_when_handle_not_enabled(): void + { + Config::set('justbetter.structured-data.runway', []); + + $parser = $this->mock(StructuredDataParser::class); + /** @var StructuredDataParser $parser */ + $service = new StructuredDataService($parser); + + $this->assertSame([], $service->getRunwayTemplateIds('product')); + } +} diff --git a/tests/Unit/Services/StructuredDataServiceTest.php b/tests/Unit/Services/StructuredDataServiceTest.php index 9cc8181..b46d249 100644 --- a/tests/Unit/Services/StructuredDataServiceTest.php +++ b/tests/Unit/Services/StructuredDataServiceTest.php @@ -9,6 +9,7 @@ use Justbetter\StatamicStructuredData\Tests\TestCase; use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; +use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\Entries\Entry; use Statamic\Facades\Collection as CollectionFacade; use Statamic\Facades\Entry as EntryFacade; @@ -682,19 +683,15 @@ public function get_templates_handles_localized_term(): void } #[Test] - public function get_templates_returns_empty_array_for_invalid_item(): void + public function get_templates_returns_empty_array_for_unsupported_entry_implementation(): void { $parser = $this->mock(StructuredDataParser::class); /** @var StructuredDataParser $parser */ $service = new StructuredDataService($parser); - $reflection = new \ReflectionClass($service); - $method = $reflection->getMethod('getTemplates'); - $method->setAccessible(true); - $result = $method->invoke($service, 'invalid-item'); + $entry = $this->mock(EntryContract::class); - $this->assertIsArray($result); - $this->assertEmpty($result); + $this->assertSame([], $service->getTemplates($entry)); } #[Test] diff --git a/tests/Unit/Support/RunwaySupportTest.php b/tests/Unit/Support/RunwaySupportTest.php new file mode 100644 index 0000000..1b26798 --- /dev/null +++ b/tests/Unit/Support/RunwaySupportTest.php @@ -0,0 +1,194 @@ +assertSame(['product', 'category'], RunwaySupport::enabledHandles()); + } + + #[Test] + public function is_handle_enabled_checks_config(): void + { + Config::set('justbetter.structured-data.runway', ['product']); + + $this->assertTrue(RunwaySupport::isHandleEnabled('product')); + $this->assertFalse(RunwaySupport::isHandleEnabled('category')); + } + + #[Test] + public function resolve_resource_handle_uses_explicit_resource(): void + { + Config::set('justbetter.structured-data.runway', ['product']); + + $model = new Product; + + $this->assertSame('product', RunwaySupport::resolveResourceHandle($model, 'product')); + $this->assertNull(RunwaySupport::resolveResourceHandle($model, 'category')); + } + + #[Test] + public function resolve_resource_handle_falls_back_to_class_basename(): void + { + Config::set('justbetter.structured-data.runway', ['runway_support_test_product']); + + $named = new RunwaySupportTestProduct; + + $this->assertSame('runway_support_test_product', RunwaySupport::resolveResourceHandle($named)); + } + + #[Test] + public function resource_options_returns_empty_when_runway_not_installed(): void + { + Config::set('justbetter.structured-data.runway', ['product']); + RunwaySupport::fakeInstalled(false); + + $this->assertSame([], RunwaySupport::resourceOptions()); + } + + #[Test] + public function resource_options_returns_empty_when_no_handles_enabled(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', []); + + $this->assertSame([], RunwaySupport::resourceOptions()); + } + + #[Test] + public function resource_options_returns_enabled_resources_when_installed(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', ['product']); + + $product = new Resource; + $product->resourceHandle = 'product'; + $product->resourceName = 'Products'; + + $category = new Resource; + $category->resourceHandle = 'category'; + $category->resourceName = 'Categories'; + + Runway::fakeResources([ + 'product' => $product, + 'category' => $category, + ]); + + $this->assertSame(['product' => 'Products'], RunwaySupport::resourceOptions()); + } + + #[Test] + public function resolve_resource_handle_uses_runway_resource_method(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', ['product']); + + $resource = new Resource; + $resource->resourceHandle = 'product'; + + $model = new Product; + $model->runwayResource = $resource; + + $this->assertSame('product', RunwaySupport::resolveResourceHandle($model)); + } + + #[Test] + public function resolve_resource_handle_ignores_disabled_runway_resource(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', ['category']); + + $resource = new Resource; + $resource->resourceHandle = 'product'; + + $model = new Product; + $model->runwayResource = $resource; + + $this->assertNull(RunwaySupport::resolveResourceHandle($model)); + } + + #[Test] + public function resolve_resource_handle_catches_runway_resource_exceptions(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', ['product']); + + $this->assertNull(RunwaySupport::resolveResourceHandle(new BrokenProduct)); + } + + #[Test] + public function resolve_resource_handle_returns_null_for_non_objects_without_explicit_handle(): void + { + $this->assertNull(RunwaySupport::resolveResourceHandle('not-an-object')); + } + + #[Test] + public function find_by_uri_returns_null_when_runway_not_installed(): void + { + RunwaySupport::fakeInstalled(false); + + $this->assertNull(RunwaySupport::findByUri('/some-product')); + } + + #[Test] + public function find_by_uri_returns_model_when_repository_finds_one(): void + { + RunwaySupport::fakeInstalled(true); + + $model = new Product; + + ModelRepository::$findByUriResult = $model; + + $this->assertSame($model, RunwaySupport::findByUri('/product')); + } + + #[Test] + public function find_by_uri_returns_null_when_repository_result_is_not_a_model(): void + { + RunwaySupport::fakeInstalled(true); + ModelRepository::$findByUriResult = 'not-a-model'; + + $this->assertNull(RunwaySupport::findByUri('/product')); + } + + #[Test] + public function find_resource_returns_null_when_not_installed_or_disabled(): void + { + Config::set('justbetter.structured-data.runway', ['product']); + RunwaySupport::fakeInstalled(false); + $this->assertNull(RunwaySupport::findResource('product')); + + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', []); + $this->assertNull(RunwaySupport::findResource('product')); + } + + #[Test] + public function find_resource_returns_resource_when_enabled(): void + { + RunwaySupport::fakeInstalled(true); + Config::set('justbetter.structured-data.runway', ['product']); + + $resource = new Resource; + $resource->resourceHandle = 'product'; + Runway::$findResults['product'] = $resource; + + $this->assertSame($resource, RunwaySupport::findResource('product')); + } +} diff --git a/tests/Unit/Tags/StructuredDataTest.php b/tests/Unit/Tags/StructuredDataTest.php index cfcb3ab..1cb71d6 100644 --- a/tests/Unit/Tags/StructuredDataTest.php +++ b/tests/Unit/Tags/StructuredDataTest.php @@ -4,6 +4,7 @@ use Justbetter\StatamicStructuredData\Actions\InjectStructuredDataAction; use Justbetter\StatamicStructuredData\Tags\StructuredData; +use Justbetter\StatamicStructuredData\Tests\Stubs\Product; use Justbetter\StatamicStructuredData\Tests\TestCase; use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; @@ -45,4 +46,42 @@ public function head_method_returns_null_when_action_returns_null(): void $this->assertNull($result); } + + #[Test] + public function for_method_passes_item_and_resource_to_action(): void + { + $model = new Product; + + $action = $this->mock(InjectStructuredDataAction::class, function (MockInterface $mock) use ($model): void { + $mock->shouldReceive('executeForItem') + ->once() + ->with($model, 'product') + ->andReturn(''); + }); + + /** @var InjectStructuredDataAction $action */ + $tag = new StructuredData($action); + $tag->setContext([]); + $tag->setParameters([ + 'item' => $model, + 'resource' => 'product', + ]); + + $this->assertSame('', $tag->for()); + } + + #[Test] + public function for_method_returns_null_when_item_missing(): void + { + $action = $this->mock(InjectStructuredDataAction::class, function (MockInterface $mock): void { + $mock->shouldNotReceive('executeForItem'); + }); + + /** @var InjectStructuredDataAction $action */ + $tag = new StructuredData($action); + $tag->setContext([]); + $tag->setParameters([]); + + $this->assertNull($tag->for()); + } }