Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions resources/dist/build/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
"src": "resources/css/statamic-structured-data.css"
},
"resources/js/statamic-structured-data.css": {
"file": "assets/statamic-structured-data-61a769e2.css",
"file": "assets/statamic-structured-data-b8c077b4.css",
"src": "resources/js/statamic-structured-data.css"
},
"resources/js/statamic-structured-data.js": {
"css": [
"assets/statamic-structured-data-61a769e2.css"
"assets/statamic-structured-data-b8c077b4.css"
],
"file": "assets/statamic-structured-data-959d3d78.js",
"file": "assets/statamic-structured-data-82617d3f.js",
"isEntry": true,
"src": "resources/js/statamic-structured-data.js"
}
Expand Down
38 changes: 31 additions & 7 deletions resources/js/components/StructuredDataObject.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
<label class="block mb-1">@id</label>
<div class="flex">
<input type="text" v-model="objectData.specialProps.id" class="input-text flex-1" :placeholder="suggestedId" />
<button class="btn ml-2" @click="useDefaultId">Use Default</button>
<button class="btn ml-2" @click="useDefaultId">{{ __('Use Default') }}</button>
</div>
</div>
</div>

<div>
<div v-for="(field, index) in objectData.fields" :key="index" class="mb-4 border-b pb-4">
<div class="flex items-start gap-2">
<div class="flex items-start gap-2">
<div class="flex-1">
<label class="block mb-1">Key</label>
<label class="block mb-1">{{ __('Key') }}</label>
<input
type="text"
v-model="field.key"
Expand Down Expand Up @@ -60,7 +60,7 @@
<input type="text" v-model="field.values[valueIndex]" class="input-text flex-1" />
<button class="btn-close" @click="removeArrayValue(field, valueIndex)">&times;</button>
</div>
<button class="btn" @click="addArrayValue(field)">Add Value</button>
<button class="btn" @click="addArrayValue(field)">{{ __('Add Value') }}</button>
</div>

<div v-else-if="field.type === 'object'" class="mt-2 border rounded p-4">
Expand All @@ -71,17 +71,29 @@
:field-key="field.key"
/>
</div>

<div v-else-if="field.type === 'replicator_object_array'" class="mt-2">
<replicator-field-mapper
v-model="field.config"
:replicator-fields="replicatorFields"
/>
</div>
</div>
</div>

<button class="btn" @click="addField">Add Property</button>
<button class="btn" @click="addField">{{ __('Add Property') }}</button>
</div>
</div>
</template>

<script>
import ReplicatorFieldMapper from './fieldtypes/ReplicatorFieldMapper.vue';

export default {
name: 'StructuredDataObject',
components: {
'replicator-field-mapper': ReplicatorFieldMapper,
},

props: {
value: {
Expand Down Expand Up @@ -125,8 +137,12 @@ export default {
{ value: 'string', label: 'String' },
{ value: 'numeric', label: 'Numeric' },
{ value: 'array', label: 'Array' },
{ value: 'object', label: 'Object' }
{ value: 'object', label: 'Object' },
{ value: 'replicator_object_array', label: 'Replicator Object Array' }
];
},
availableReplicatorFields() {
return this.replicatorFields || [];
}
},

Expand Down Expand Up @@ -167,7 +183,8 @@ export default {
type: 'string',
value: '',
values: [],
fields: []
fields: [],
config: {}
});
},

Expand Down Expand Up @@ -205,6 +222,13 @@ export default {
});
} else if (field.type === 'array') {
field.values = [];
} else if (field.type === 'replicator_object_array') {
field.config = {
replicator_field: '',
set: '',
mappings: []
};
field.values = [];
} else {
field.value = '';
}
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/fieldtypes/AvailableVariables.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
</div>
</div>
</div>
<div v-show="tooltipVisible" class="tooltip" :style="tooltipStyle">Copied!</div>
<div v-show="tooltipVisible" class="tooltip" :style="tooltipStyle">{{ __('Copied!') }}</div>
</div>
</template>

Expand Down
261 changes: 261 additions & 0 deletions resources/js/components/fieldtypes/ReplicatorFieldMapper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
<template>
<div class="replicator-mapper space-y-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="text-gray-600 mb-1 block">{{ __('Replicator Field') }}</label>
<v-select
v-model="localConfig.replicator_field"
:options="replicatorFieldOptions"
@input="(val) => { localConfig.replicator_field = val ? val.value : ''; localConfig.set = ''; }"
:placeholder="replicatorFieldOptions.length > 0 ? __('Select replicator field') : __('No replicator fields available')"
:disabled="replicatorFieldOptions.length === 0"
/>
<div v-if="replicatorFieldOptions.length === 0" class="text-xs text-yellow-600 mt-1 bg-yellow-50 p-2 rounded">
<strong>{{ __('No replicator fields found.') }}</strong><br>
{{ __('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.') }}
</div>
</div>
<div>
<label class="text-gray-600 mb-1 block">{{ __('Limit to Set (optional)') }}</label>
<v-select
v-model="localConfig.set"
:options="setOptions"
@input="(val) => { localConfig.set = val ? val.value : ''; }"
:placeholder="__('All sets')"
:clearable="true"
/>
</div>
</div>

<div class="border rounded p-3">
<div class="flex items-center justify-between mb-2">
<h4 class="font-semibold text-gray-700">{{ __('Field Mappings') }}</h4>
<button class="btn-primary text-sm" @click="addMapping">{{ __('Add Mapping') }}</button>
</div>

<div v-if="!localConfig.mappings.length" class="text-sm text-gray-500">
{{ __('No mappings yet. Add one to map replicator fields into your JSON-LD object.') }}
</div>

<div
v-for="(mapping, index) in localConfig.mappings"
:key="index"
class="border rounded mb-3 p-3"
>
<div class="flex items-start gap-2">
<div class="flex-1">
<label class="text-gray-600 mb-1 block">{{ __('JSON-LD Key') }}</label>
<input
type="text"
v-model="mapping.key"
class="input-text w-full"
placeholder="e.g. name"
@input="sanitizeKey(mapping)"
/>
</div>
<div class="w-40">
<label class="text-gray-600 mb-1 block">{{ __('Source') }}</label>
<v-select
v-model="mapping.mode"
:options="modeOptions"
@input="(val) => { mapping.mode = val.value; }"
/>
</div>
<button class="btn-danger mt-6" @click="removeMapping(index)">{{ __('Remove') }}</button>
</div>

<div class="mt-2">
<template v-if="mapping.mode === 'static'">
<label class="text-gray-600 mb-1 block">{{ __('Static Value') }}</label>
<input
type="text"
v-model="mapping.static"
class="input-text w-full"
:placeholder="__('e.g. PropertyValue')"
/>
</template>

<template v-else-if="mapping.mode === 'field'">
<label class="text-gray-600 mb-1 block">{{ __('Replicator Field') }}</label>
<v-select
v-model="mapping.field"
:options="getFieldOptionsForMapping(mapping)"
@input="(val) => { mapping.field = val ? val.value : ''; }"
:placeholder="__('Select field')"
/>
</template>

<template v-else-if="mapping.mode === 'nested_replicator'">
<div class="p-3 border rounded">
<replicator-field-mapper
v-model="mapping.nested"
:replicator-fields="replicatorFields"
/>
</div>
</template>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
name: 'ReplicatorFieldMapper',
props: {
value: {
type: Object,
default: () => ({
replicator_field: '',
set: '',
mappings: []
})
},
replicatorFields: {
type: Array,
default: () => []
}
},
data() {
return {
localConfig: this.normalizeConfig(this.value),
modeOptions: [
{ value: 'field', label: 'From Replicator Field' },
{ value: 'static', label: 'Static Value' },
{ value: 'nested_replicator', label: 'Nested Replicator' },
],
}
},
computed: {
availableReplicatorFields() {
if (!this.replicatorFields || !Array.isArray(this.replicatorFields)) {
return [];
}
return this.replicatorFields;
},
replicatorFieldOptions() {
if (!this.availableReplicatorFields || this.availableReplicatorFields.length === 0) {
return [];
}
return this.availableReplicatorFields.map(field => ({
value: field.handle,
label: field.display || field.handle
}));
},
selectedReplicatorField() {
if (!this.localConfig.replicator_field) {
return null;
}
return this.availableReplicatorFields.find(
field => field.handle === this.localConfig.replicator_field
);
},
setOptions() {
if (!this.selectedReplicatorField) {
return [];
}
return this.selectedReplicatorField.sets.map(set => ({
value: set.value,
label: set.label
}));
}
},
watch: {
localConfig: {
deep: true,
handler(val) {
const newVal = JSON.stringify(val);
const oldVal = JSON.stringify(this.value);

if (newVal !== oldVal) {
this.$emit('input', JSON.parse(newVal));
}
}
},
value: {
deep: true,
handler(val) {
const newVal = JSON.stringify(this.normalizeConfig(val));
const oldVal = JSON.stringify(this.localConfig);

if (newVal !== oldVal) {
this.localConfig = JSON.parse(newVal);
}
}
}
},
methods: {
normalizeConfig(config) {
const base = {
replicator_field: '',
set: '',
mappings: []
};

if (!config || typeof config !== 'object') {
return base;
}

return {
replicator_field: config.replicator_field || '',
set: config.set || '',
mappings: Array.isArray(config.mappings)
? JSON.parse(JSON.stringify(config.mappings))
: []
};
},
addMapping() {
this.localConfig.mappings.push({
key: '',
mode: 'field',
field: '',
static: '',
nested: {
replicator_field: '',
set: '',
mappings: []
}
});
},
removeMapping(index) {
this.localConfig.mappings.splice(index, 1);
},
sanitizeKey(mapping) {
mapping.key = mapping.key.replace(/[^a-zA-Z0-9@]/g, '');
},
getFieldOptionsForMapping(mapping) {
if (!this.selectedReplicatorField) {
return [];
}

const sets = this.selectedReplicatorField.sets || [];
let allFields = [];

if (this.localConfig.set) {
const selectedSet = sets.find(set => set.value === this.localConfig.set);
if (selectedSet && selectedSet.fields) {
allFields = selectedSet.fields;
}
} else {
sets.forEach(set => {
if (set.fields) {
allFields = allFields.concat(set.fields);
}
});
}

const uniqueFields = [];
const seenValues = new Set();
allFields.forEach(field => {
if (!seenValues.has(field.value)) {
seenValues.add(field.value);
uniqueFields.push(field);
}
});

return uniqueFields;
}
}
}
</script>

Loading