Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Dictionaries #10380

Merged
merged 80 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
2b8e531
Working prototype of dictionaries feature
duncanmcclean Jun 7, 2024
836b529
wip
duncanmcclean Jun 7, 2024
ee81cd2
Make it possible to search dictionaries
duncanmcclean Jun 20, 2024
2ea020a
wip
duncanmcclean Jun 20, 2024
0e3c63d
wip
duncanmcclean Jun 20, 2024
47136ee
tweaks
duncanmcclean Jun 21, 2024
5f49d48
wip
duncanmcclean Jun 21, 2024
923b804
Leave this return type open
duncanmcclean Jun 21, 2024
f969de2
Timezone dictionary
duncanmcclean Jun 21, 2024
4cc6bfe
tweaks to the fieldtype
duncanmcclean Jun 21, 2024
4ac1f23
wip
duncanmcclean Jun 21, 2024
19bb84d
Forms support
duncanmcclean Jun 21, 2024
8ae22f7
Allow adding config fields to dictionaries
duncanmcclean Jul 1, 2024
040912e
wip
duncanmcclean Jul 2, 2024
482bf42
wip
duncanmcclean Jul 2, 2024
082cec3
Add `make:dictionary` command
duncanmcclean Jul 2, 2024
6583ff8
Make these comments align with the comments in the stub
duncanmcclean Jul 2, 2024
ce4d3c7
simplify
duncanmcclean Jul 2, 2024
3578bf4
Add currencies dictionary
duncanmcclean Jul 2, 2024
aa24f33
Merge remote-tracking branch 'origin/5.x' into dictionaries
duncanmcclean Jul 2, 2024
0641a34
Undo changes by PHPStorm's refactoring feature
duncanmcclean Jul 2, 2024
0407218
Fix styling
duncanmcclean Jul 2, 2024
11e0fd2
wip
duncanmcclean Jul 2, 2024
aab430e
Merge branch 'dictionaries' of github.com:statamic/cms into dictionaries
duncanmcclean Jul 2, 2024
8059e04
remove unrelated change
duncanmcclean Jul 2, 2024
71601ed
dictionaries don't have scripts
duncanmcclean Jul 2, 2024
540e07a
rename variable in facade docblock
duncanmcclean Jul 2, 2024
f2aadc1
it's on my to-do list - remove the todo
duncanmcclean Jul 2, 2024
85a1452
Give the fieldtype a proper icon
duncanmcclean Jul 2, 2024
00a7701
wip
duncanmcclean Jul 2, 2024
a7f0ebd
Backfill tests
duncanmcclean Jul 2, 2024
52fe753
Fix styling
duncanmcclean Jul 2, 2024
eac7417
Merge remote-tracking branch 'origin/5.x' into dictionaries
duncanmcclean Jul 16, 2024
0225a9b
words
jasonvarga Jul 23, 2024
1224950
assume we always get an array
jasonvarga Jul 23, 2024
5df5d57
timezone returns the name and utc offset
jasonvarga Jul 23, 2024
6147d4c
initial graphql
jasonvarga Jul 23, 2024
644bb8d
simplify!
jasonvarga Jul 23, 2024
7d972b9
scrap dedicated `multiple` option in favor of max_items 1 like relati…
jasonvarga Jul 24, 2024
492dc34
match name to fieldtype
jasonvarga Jul 24, 2024
78c5d09
rename and test repo directly
jasonvarga Jul 24, 2024
74bdacb
add individual dictionary tests and support currency symbol and count…
jasonvarga Jul 24, 2024
1bf2820
Merge branch '5.x' into dictionaries
jasonvarga Jul 24, 2024
2115460
show offsets and allow searching them
jasonvarga Jul 24, 2024
746c068
map to whats in the get method
jasonvarga Jul 24, 2024
7b476d3
avoid procedural
jasonvarga Jul 24, 2024
3cd20ec
change decimal_digits to decimals
jasonvarga Jul 24, 2024
d84be11
just put it in php to avoid constantly reading never-changing json file
jasonvarga Jul 24, 2024
f482e24
countries too
jasonvarga Jul 24, 2024
2faf81f
tidy
jasonvarga Jul 24, 2024
befb180
extract method
jasonvarga Jul 24, 2024
4bbd9d2
fix unicode pasted from json file
jasonvarga Jul 24, 2024
f9796a4
extract method here too
jasonvarga Jul 24, 2024
1acbff2
extract a "basic" dictionary class to drastically simplify dictionaries
jasonvarga Jul 24, 2024
12800b9
add item class which is a labeledvalue, which allows output as string…
jasonvarga Jul 25, 2024
72b6736
Infer more gql types
jasonvarga Jul 25, 2024
84125c4
make the region options lowercase
jasonvarga Jul 25, 2024
0887adc
invalid values get filtered out, like relationship fields
jasonvarga Jul 25, 2024
2d01de8
invalid options get highlighted in the ui
jasonvarga Jul 25, 2024
fd0c0b0
there was a test for that, nice.
jasonvarga Jul 25, 2024
9adb82d
fix double key
jasonvarga Jul 25, 2024
a2f5fa3
clean up and test getting dictionary instance
jasonvarga Jul 25, 2024
6bba990
label_html was copied from select fieldtype. we dont need it. just as…
jasonvarga Jul 25, 2024
2b84022
always clearable
jasonvarga Jul 25, 2024
10038fd
wip
jasonvarga Jul 25, 2024
93eadfa
always searchable
jasonvarga Jul 25, 2024
b2e22be
wip
jasonvarga Jul 25, 2024
b1585c9
Add File dictionary to let users point at a file
jasonvarga Jul 25, 2024
1f9e2ce
call it config
jasonvarga Jul 25, 2024
fafab0c
arr take not available on lowest version. dont feel like upping the r…
jasonvarga Jul 26, 2024
9167050
Improve file dictionary config fields
jasonvarga Jul 26, 2024
e177fb1
breathe
jasonvarga Jul 26, 2024
adb8074
error when file doesnt exist
jasonvarga Jul 26, 2024
eb1aaf8
csv support
jasonvarga Jul 26, 2024
2b67fe9
csv test
jasonvarga Jul 29, 2024
838046b
debounce searching
jasonvarga Jul 29, 2024
598728d
expose multiple bool to fields so it renders properly, now that the d…
jasonvarga Jul 29, 2024
278b0e2
not sure why this was necessary
jasonvarga Jul 29, 2024
cd1855f
nitpick
jasonvarga Jul 29, 2024
d6f8e69
move into dedicated css file to avoid rollup-inject warning ...
jasonvarga Jul 29, 2024
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
20 changes: 20 additions & 0 deletions resources/css/components/fieldtypes/dictionary-fields.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.dictionary_fields-fieldtype {
@apply p-0;

.publish-fields {
@apply w-full;
}

.config-field {
@apply md:flex flex-wrap border-b border-gray-400 dark:border-dark-900 w-full;
@apply p-3 @sm:p-4 m-0;

.field-inner {
@apply w-full md:w-1/2 rtl:md:pl-8 ltr:md:pr-8;
}

.field-inner + div {
@apply w-full md:w-1/2;
}
}
}
1 change: 1 addition & 0 deletions resources/css/cp.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
@import "components/fieldtypes/checkboxes";
@import "components/fieldtypes/code";
@import "components/fieldtypes/datetime";
@import "components/fieldtypes/dictionary-fields";
@import "components/fieldtypes/environment";
@import "components/fieldtypes/grid";
@import "components/fieldtypes/hidden";
Expand Down
6 changes: 6 additions & 0 deletions resources/css/vendors/vue-select.css
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@

&.sortable-item { @apply !cursor-grab; }

&.invalid {
@apply border-red-300 dark:border-dark-red bg-red-100 dark:bg-red-400 text-red-500 dark:text-red-950;
background-image: none;

.vs__deselect { @apply text-red-500 dark:text-red-950 }
}
}

.vs__deselect {
Expand Down
4 changes: 4 additions & 0 deletions resources/js/bootstrap/fieldtypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import Routes from '../components/collections/Routes.vue';
import TitleFormats from '../components/collections/TitleFormats.vue';
import ColorFieldtype from '../components/fieldtypes/ColorFieldtype.vue';
import DateFieldtype from '../components/fieldtypes/DateFieldtype.vue';
import DictionaryFieldtype from "../components/fieldtypes/DictionaryFieldtype.vue";
import DictionaryFields from "../components/fieldtypes/DictionaryFields.vue";
import FieldDisplayFieldtype from '../components/fieldtypes/FieldDisplayFieldtype.vue';
import FieldsFieldtype from '../components/fieldtypes/grid/FieldsFieldtype.vue';
import FilesFieldtype from '../components/fieldtypes/FilesFieldtype.vue';
Expand Down Expand Up @@ -86,6 +88,8 @@ Vue.component('collection_routes-fieldtype', Routes);
Vue.component('collection_title_formats-fieldtype', TitleFormats);
Vue.component('color-fieldtype', ColorFieldtype);
Vue.component('date-fieldtype', DateFieldtype);
Vue.component('dictionary-fieldtype', DictionaryFieldtype);
Vue.component('dictionary_fields-fieldtype', DictionaryFields);
Vue.component('field_display-fieldtype', FieldDisplayFieldtype);
Vue.component('fields-fieldtype', FieldsFieldtype);
Vue.component('files-fieldtype', FilesFieldtype);
Expand Down
82 changes: 82 additions & 0 deletions resources/js/components/fieldtypes/DictionaryFields.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<template>
<publish-container
name="dictionary-fields"
:blueprint="blueprint"
:values="value"
:meta="publishMeta"
:is-config="true"
:errors="errors"
@updated="update"
>
<publish-fields
slot-scope="{ setFieldValue, setFieldMeta }"
:fields="fields"
@updated="setFieldValue"
@meta-updated="setFieldMeta"
/>
</publish-container>
</template>

<script>
import Fieldtype from "./Fieldtype.vue";

export default {
mixins: [Fieldtype],

inject: ['storeName'],

computed: {
dictionary() {
return this.value?.type;
},

fields() {
return this.meta.type.fields.concat(this.meta.dictionaries[this.dictionary]?.fields || [])
},

blueprint() {
return {
tabs: [{
fields: this.fields
}]
}
},

publishMeta() {
return {
...this.meta.type.meta,
...this.meta.dictionaries[this.dictionary]?.meta
}
},

errors() {
const state = this.$store.state.publish[this.storeName];

if (! state) {
return {};
}

let errors = {}

// Filter errors to only include those for this field, and remove the field path prefix
// if there is one, then append it to the errors object.
Object.entries(state.errors)
.filter(([key, value]) => key.startsWith(this.fieldPathPrefix || this.handle))
.forEach(([key, value]) => {
errors[key.split('.').pop()] = value
})

return errors
},
},

watch: {
dictionary() {
this.update({
type: dictionary,
...this.meta.dictionaries[this.dictionary]?.defaults
})
},
},
}
</script>
200 changes: 200 additions & 0 deletions resources/js/components/fieldtypes/DictionaryFieldtype.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<template>
<div class="flex">
<v-select
ref="input"
:input-id="fieldId"
class="flex-1"
append-to-body
searchable
close-on-select
:calculate-position="positionOptions"
:name="name"
:disabled="config.disabled || isReadOnly || (multiple && limitReached)"
:options="normalizeInputOptions(options)"
:placeholder="__(config.placeholder)"
:multiple="multiple"
:value="selectedOptions"
@input="vueSelectUpdated"
@focus="$emit('focus')"
@search="search"
@search:focus="$emit('focus')"
@search:blur="$emit('blur')">
<template #selected-option-container v-if="multiple"><i class="hidden"></i></template>
<template #search="{ events, attributes }" v-if="multiple">
<input
:placeholder="__(config.placeholder)"
class="vs__search"
type="search"
v-on="events"
v-bind="attributes"
>
</template>
<template #option="{ label }">
<div v-html="label" />
</template>
<template #selected-option="{ label }">
<div v-html="label" />
</template>
<template #no-options>
<div class="text-sm text-gray-700 rtl:text-right ltr:text-left py-2 px-4" v-text="__('No options to choose from.')" />
</template>
<template #footer="{ deselect }" v-if="multiple">
<sortable-list
item-class="sortable-item"
handle-class="sortable-item"
:value="value"
:distance="5"
:mirror="false"
@input="update"
>
<div class="vs__selected-options-outside flex flex-wrap">
<span v-for="option in selectedOptions" :key="option.value" class="vs__selected mt-2 sortable-item" :class="{'invalid': option.invalid}">
<div v-html="option.label" />
<button v-if="!readOnly" @click="deselect(option)" type="button" :aria-label="__('Deselect option')" class="vs__deselect">
<span>×</span>
</button>
<button v-else type="button" class="vs__deselect">
<span class="text-gray-500">×</span>
</button>
</span>
</div>
</sortable-list>
</template>
</v-select>
<div class="text-xs rtl:mr-2 ltr:ml-2 mt-3" :class="limitIndicatorColor" v-if="config.max_items > 1">
<span v-text="currentLength"></span>/<span v-text="config.max_items"></span>
</div>
</div>
</template>

<style scoped>
.draggable-source--is-dragging {
@apply opacity-75 bg-transparent border-dashed
}
</style>

<script>
import HasInputOptions from './HasInputOptions.js'
import { SortableList } from '../sortable/Sortable';
import PositionsSelectOptions from '../../mixins/PositionsSelectOptions';

export default {

mixins: [Fieldtype, HasInputOptions, PositionsSelectOptions],

components: {
SortableList
},

data() {
return {
options: {},
selectedOptionData: this.meta.selectedOptions,
}
},

computed: {
multiple() {
return this.config.max_items !== 1;
},

selectedOptions() {
let selections = this.value || [];

if (typeof selections === 'string' || typeof selections === 'number') {
selections = [selections];
}

return selections.map(value => {
let option = this.selectedOptionData.find(option => option.value === value);

if (! option) return {value, label: value};

return {value: option.value, label: option.label, invalid: option.invalid};
});
},

replicatorPreview() {
if (! this.showFieldPreviews || ! this.config.replicator_preview) return;

return this.selectedOptions.map(option => option.label).join(', ');
},

limitReached() {
if (!this.config.max_items) return false;

return this.currentLength >= this.config.max_items;
},

limitExceeded() {
if (!this.config.max_items) return false;

return this.currentLength > this.config.max_items;
},

currentLength() {
if (this.value) {
return (typeof this.value == 'string') ? 1 : this.value.length;
}

return 0;
},

limitIndicatorColor() {
if (this.limitExceeded) {
return 'text-red-500';
} else if (this.limitReached) {
return 'text-green-600';
}

return 'text-gray';
},

configParameter() {
return utf8btoa(JSON.stringify(this.config));
},
},

mounted() {
this.request();
},

methods: {
focus() {
this.$refs.input.focus();
},

vueSelectUpdated(value) {
if (this.multiple) {
this.update(value.map(v => v.value));
value.forEach((option) => this.selectedOptionData.push(option));
} else {
if (value) {
this.update(value.value)
this.selectedOptionData.push(value)
} else {
this.update(null);
}
}
},

request(params = {}) {
params = {
config: this.configParameter,
...params,
}

return this.$axios.get(this.meta.url, { params }).then(response => {
this.options = response.data.data;
return Promise.resolve(response);
});
},

search: _.debounce(function (search, loading) {
loading(true);

this.request({ search }).then(response => loading(false));
}, 300),
}
};
</script>
4 changes: 4 additions & 0 deletions resources/lang/en/fieldtypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
'date.config.time_enabled' => 'Enable the timepicker.',
'date.config.time_seconds_enabled' => 'Show seconds in the timepicker.',
'date.title' => 'Date',
'dictionary.config.dictionary' => 'The dictionary you wish to pull options from.',
'dictionary.file.config.filename' => 'The filename containing your options, relative to the `resources/dictionaries` directory.',
'dictionary.file.config.label' => "The key containing the options' labels. By default it's `label`. Alternatively, you may use Antlers.",
'dictionary.file.config.value' => "The key containing the options' values. By default it's `value`.",
'entries.config.create' => 'Allow creation of new entries.',
'entries.config.collections' => 'Choose which collections the user can select from.',
'entries.config.query_scopes' => 'Choose which query scopes should be applied when retrieving selectable entries.',
Expand Down
1 change: 1 addition & 0 deletions resources/lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
'collections_sort_direction_instructions' => 'The default sort direction.',
'collections_preview_target_refresh_instructions' => 'Automatically refresh the preview while editing. Disabling this will use postMessage.',
'collections_taxonomies_instructions' => 'Connect entries in this collection to taxonomies. Fields will be automatically added to publish forms.',
'dictionaries_countries_region_instructions' => 'Optionally filter the countries by region.',
'duplicate_action_warning_localization' => 'This entry is a localization. The origin entry will be duplicated.',
'duplicate_action_warning_localizations' => 'One or more selected entries are localizations. In those cases, the origin entry will be duplicated instead.',
'duplicate_action_localizations_confirmation' => 'Are you sure you want to run this action? Localizations will also be duplicated.',
Expand Down
1 change: 1 addition & 0 deletions resources/svg/icons/light/dictionary.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions resources/views/extend/forms/fields/dictionary.antlers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<select
name="{{ handle }}{{ multiple ?= "[]" }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if multiple }}multiple{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
>
{{ unless multiple }}
<option value>
{{ if placeholder }}
{{ placeholder }}
{{ else }}
{{ trans key="Please select..." }}
{{ /if }}
</option>
{{ /unless }}
{{ foreach:options as="option|label" }}
<option value="{{ option }}"{{ if multiple }}{{ if value|in_array:option }} selected{{ /if }}{{ else }}{{ if option == value }} selected{{ /if }}{{ /if }}>
{{ label !== null ? label : option }}
</option>
{{ /foreach:options }}
</select>
2 changes: 1 addition & 1 deletion resources/views/forms/automagic-email.antlers.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{{ elseif fieldtype == "radio" }}
{{ value:label ?? value }}

{{ elseif fieldtype == "select" || fieldtype == "checkboxes" }}
{{ elseif fieldtype == "select" || fieldtype == "checkboxes" || fieldtype == "dictionary" }}
{{ value }}{{ label ?? value }}{{ if !last }}, {{ /if }}{{ /value }}

{{ elseif value|is_iterable }}
Expand Down
Loading
Loading