') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); [4.x] Filters refinements by jesseleite · Pull Request #8001 · statamic/cms · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b61d71f
Our listing filters UI got minimalized a step too far. splitting the …
jackmcdade Apr 18, 2023
eeb3769
Revert "Remove `$pinned` property from filters in php."
jesseleite Apr 18, 2023
faca7fa
Revert "Remove `$pinned` property from new filter stub."
jesseleite Apr 18, 2023
985140e
Field filter popover doesn’t deal with standard filters.
jesseleite Apr 19, 2023
c1e298a
Make fields filter pinned by default.
jesseleite Apr 19, 2023
f169fc8
Finish bringing back unpinned filters to new UX.
jesseleite Apr 19, 2023
aceced6
Nit.
jesseleite Apr 19, 2023
6929edf
Merge branch 'master' into un-minimialize-filters
jackmcdade Apr 24, 2023
30e4f79
Pin `Blueprint` filter by default.
jesseleite Apr 24, 2023
5beb48b
Pin User `Role` and `Group` filters by default (for when we circle ba…
jesseleite Apr 24, 2023
48d925a
Pin `Collection` filter by default (only visible in `entries` fieldty…
jesseleite Apr 24, 2023
56571cc
Make consistent with dynamically generated filter popovers.
jesseleite Apr 24, 2023
736b8cf
Add filter preset dropdowns.
jesseleite Apr 25, 2023
e50288d
Use new methods.
jesseleite Apr 25, 2023
75bb23b
Rename preset gets its own modal.
jesseleite Apr 25, 2023
327e7ef
Simplify and rework preset action methods.
jesseleite Apr 25, 2023
8d0bbcb
HItting reset button in listing should refresh current preset, not re…
jesseleite Apr 25, 2023
6a15d41
Fix 'Reset' button when 'All' is selected.
jesseleite Apr 26, 2023
8b1fc08
Port filter changes to users listing.
jesseleite Apr 26, 2023
f12d477
apply filter ui to stack selector
jasonvarga Apr 26, 2023
b4e9818
remove now-unused events
jasonvarga Apr 26, 2023
f6b9671
Remove more unused method and events.
jesseleite Apr 26, 2023
4c3ef75
Port filter changes to terms listing.
jesseleite Apr 26, 2023
a7a0703
Remove unused events here too.
jesseleite Apr 26, 2023
2fb3f4b
Rename `filter-changed` event to `changed`.
jesseleite Apr 26, 2023
4b01ea6
Allow fields filter on user group users listing.
jesseleite Apr 26, 2023
d59206a
Allow search query logic on user group users listing.
jesseleite Apr 26, 2023
468fb1b
Fix fields filter dropdown on user group users listing.
jesseleite Apr 26, 2023
d5cc804
Disable filter presets on user group users listing.
jesseleite Apr 26, 2023
0d4af10
Scope these wheres into one nested where to prevent conflicts.
jesseleite Apr 26, 2023
5062940
Don’t use search index on user group users listing.
jesseleite Apr 26, 2023
8f0ce0c
Add tooltip to explain the `+` button when you have no saved views.
jesseleite Apr 26, 2023
81f0f98
Respect title on filter class (in case someone binds custom implement…
jesseleite Apr 27, 2023
0d639b6
Misc cleanup.
jesseleite Apr 27, 2023
579e43e
Fix submissions listing search.
jesseleite Apr 27, 2023
92fbe35
Merge branch 'master' of https://github.com/statamic/cms into un-mini…
jesseleite Apr 27, 2023
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
4 changes: 2 additions & 2 deletions resources/css/elements/badges.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@
}

.filter-badge {
@apply text-2xs text-gray-800 bg-white border flex items-center rounded-full;
@apply text-2xs text-gray-800 bg-white border border-dashed border-gray-600 flex items-center rounded-full;
height: 1.625rem;
padding-left: 10px;

Expand All@@ -44,6 +44,6 @@
}

&.filter-badge-control {
@apply bg-gray-200 border border-gray-500 border-dashed hover:border-gray-600 hover:bg-gray-300;
@apply bg-gray-100 border border-solid rounded-full border-gray-500 hover:border-gray-600 hover:bg-gray-200;
}
}
4 changes: 2 additions & 2 deletions resources/js/components/data-list/FieldFilter.vue
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
<template>
<div>
<div v-if="hasAvailableFieldFilters">
<div class="flex flex-col p-4">
<div class="flex flex-col p-3">

<v-select
ref="fieldSelect"
:placeholder="__('Select field')"
:placeholder="__('Field')"
:options="fieldOptions"
:reduce="option => option.value"
:value="field"
Expand Down
136 changes: 84 additions & 52 deletions resources/js/components/data-list/FilterPresets.vue
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,52 @@
<template>
<div class="pr-2">
<div class="pt-2 pr-2">
<div class="flex flex-wrap items-center">

<button class="pill-tab mr-1" :class="{ 'active': ! activePreset }" @click="viewAll" v-text="__('All')" />

<template v-for="(preset, handle) in presets">
<button class="pill-tab active mr-1" v-if="handle === activePreset">
{{ preset.display }}
<button class="opacity-50 hover:opacity-100" @click="showDeleteModal = true">
<span class="w-2 h-2 ml-2 mr-1">&times;</span>
</button>
<dropdown-list class="ml-2" placement="bottom-start">
<template v-slot:trigger>
<button class="opacity-50 hover:opacity-100">
<svg-icon name="micro/chevron-down-xs" class="w-2 h-2" />
</button>
</template>
<dropdown-item :text="__('Duplicate')" @click="createPreset" />
<dropdown-item :text="__('Rename')" @click="renamePreset" />
<div class="divider" />
<dropdown-item :text="__('Delete')" class="warning" @click="deletePreset" />
</dropdown-list>
</button>
<button class="pill-tab mr-1" v-else @click="viewPreset(handle)">
{{ preset.display }}
</button>
</template>

<button class="pill-tab" @click="createNewEmptyPreset" >
<button class="pill-tab" @click="createPreset" v-tooltip="__('Create New View')">
<svg-icon name="add" class="w-3 h-3"/>
</button>
</div>

<confirmation-modal
:buttonText="__('Save')"
v-if="showCreateModal"
:title="__('Create New View')"
v-if="showSaveModal"
@cancel="showSaveModal = false"
@confirm="handleSavePreset"
:buttonText="__('Create')"
@cancel="showCreateModal = false"
@confirm="savePreset(savingPresetSlug)"
>
<text-input :focus="true" v-model="savingPresetName" @keydown.enter="handleSavePreset" />
<text-input :focus="true" v-model="savingPresetName" @keydown.enter="savePreset(savingPresetSlug)" />
</confirmation-modal>

<confirmation-modal
v-if="showRenameModal"
:title="__('Rename View')"
:buttonText="__('Rename')"
@cancel="showRenameModal = false"
@confirm="savePreset()"
>
<text-input :focus="true" v-model="savingPresetName" @keydown.enter="savePreset()" />
</confirmation-modal>

<confirmation-modal
Expand DownExpand Up@@ -58,8 +76,9 @@ export default {
data() {
return {
presets: [],
showCreateModal: false,
showRenameModal: false,
showDeleteModal: false,
showSaveModal: false,
savingPresetName: null,
test: 'hello!'
}
Expand All@@ -80,6 +99,10 @@ export default {

return payload;
},

savingPresetSlug() {
return this.$slugify(this.savingPresetName, '_');
},
},

created() {
Expand All@@ -98,69 +121,78 @@ export default {
this.viewPreset(handle)
},

deletePreset() {
this.$preferences.remove(`${this.preferencesKey}.${this.activePreset}`)
.then(response => {
this.$emit('deleted', this.activePreset);
this.$toast.success(__('View deleted'));
this.showDeleteModal = false;
this.refreshPresets();
})
.catch(error => {
this.$toast.error(__('Unable to delete view'));
this.showDeleteModal = false;
});
refreshPresets() {
this.getPresets();
this.viewAll();
},

savePreset() {
if (!this.activePreset) {
this.showSaveModal = true;
return;
refreshPreset() {
if (this.activePreset) {
this.setPreset(this.activePreset);
} else {
this.viewAll();
}
},

this.handleSavePreset();
viewAll() {
this.$emit('reset');
},

createNewEmptyPreset() {
viewPreset(handle) {
this.$emit('selected', handle, this.presets[handle]);
},

createPreset() {
this.savingPresetName = null;
this.showSaveModal = true;
this.showCreateModal = true;
},

renamePreset() {
this.savingPresetName = this.activePresetPayload.display;
this.showRenameModal = true;
},

handleSavePreset() {
let presetHandle = this.activePreset || this.$slugify(this.savingPresetName, '_');
savePreset(handle) {
let presetHandle = handle || this.activePreset;

if (! presetHandle) {
this.showCreateModal = true;
return;
}

this.$preferences.set(`${this.preferencesKey}.${presetHandle}`, this.presetPreferencesPayload)
.then(response => {
this.$toast.success(__('View saved'));
this.showSaveModal = false;
this.getPresets();
this.viewPreset(presetHandle);
this.hasActiveFilters ? this.refreshPreset() : this.$emit('show-filters');
this.showCreateModal = false;
this.showRenameModal = false;
this.setPreset(presetHandle);
})
.catch(error => {
this.$toast.error(__('Unable to save view'));
this.showSaveModal = false;
this.showCreateModal = false;
this.showRenameModal = false;
});
},

refreshPresets() {
this.getPresets();
this.viewAll();
},

refreshPreset() {
this.getPresets();
this.$emit('hide-filters');
this.viewPreset(this.activePreset);
},
deletePreset() {
if (! this.showDeleteModal) {
this.showDeleteModal = true;
return;
}

viewAll() {
this.$emit('reset');
this.$preferences.remove(`${this.preferencesKey}.${this.activePreset}`)
.then(response => {
this.$emit('deleted', this.activePreset);
this.$toast.success(__('View deleted'));
this.showDeleteModal = false;
this.refreshPresets();
})
.catch(error => {
this.$toast.error(__('Unable to delete view'));
this.showDeleteModal = false;
});
},

viewPreset(handle) {
this.$emit('selected', handle, this.presets[handle]);
},
},

}
Expand Down
92 changes: 67 additions & 25 deletions resources/js/components/data-list/Filters.vue
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
<template>
<div class="w-full" v-if="isFiltering || isSearching">
<div class="flex flex-wrap px-3 border-b pt-2">
<div class="shadow-inner bg-gray-300">
<div class="flex items-center flex-wrap px-3 border-b pt-2">

<!-- Field filter (requires custom selection UI) -->
<popover v-if="fieldFilter" placement="bottom-start" @closed="fieldFilterClosed">
<template slot="trigger">
<button class="filter-badge filter-badge-control mr-2 mb-2" @click="resetFilterPopover">
{{ __('Field') }}
{{ fieldFilter.title }}
<svg-icon name="micro/chevron-down-xs" class="w-2 h-2 mx-2" />
</button>
</template>
Expand All@@ -18,17 +18,7 @@
:config="fieldFilter"
:values="activeFilters.fields || {}"
:badges="fieldFilterBadges"
@changed="$emit('filter-changed', {handle: 'fields', values: $event})"
@cleared="creating = false"
@closed="closePopover"
/>
<data-list-filter
v-for="filter in standardFilters"
v-if="creating === filter.handle"
:key="filter.handle"
:filter="filter"
:values="activeFilters[filter.handle]"
@changed="$emit('filter-changed', {handle: filter.handle, values: $event})"
@changed="$emit('changed', {handle: 'fields', values: $event})"
@cleared="creating = false"
@closed="closePopover"
/>
Expand All@@ -37,8 +27,8 @@
</template>
</popover>

<!-- Standard non-field filters -->
<popover v-if="standardFilters.length" v-for="filter in standardFilters" :key="filter.handle" placement="bottom-start">
<!-- Standard pinned filters -->
<popover v-if="pinnedFilters.length" v-for="filter in pinnedFilters" :key="filter.handle" placement="bottom-start" :stop-propagation="false">
<template slot="trigger">
<button class="filter-badge filter-badge-control mr-2 mb-2">
{{ filter.title }}
Expand All@@ -51,13 +41,49 @@
:key="filter.handle"
:filter="filter"
:values="activeFilters[filter.handle]"
@changed="$emit('filter-changed', {handle: filter.handle, values: $event})"
@changed="$emit('changed', {handle: filter.handle, values: $event})"
@closed="closePopover"
/>
</div>
</template>
</popover>

<!-- Standard unpinned filters -->
<popover v-if="unpinnedFilters.length" placement="bottom-start" :stop-propagation="false">
<template slot="trigger">
<button class="filter-badge filter-badge-control mr-2 mb-2" @click="resetFilterPopover">
{{ __('Filter') }}
<svg-icon name="micro/chevron-down-xs" class="w-2 h-2 mx-2" />
</button>
</template>
<template #default="{ close: closePopover }">
<div class="filter-fields w-64">
<h6 v-text="creatingFilterHeader" class="p-3 pb-0" />
<div v-if="showUnpinnedFilterSelection" class="p-3 pt-1">
<button
v-for="filter in unpinnedFilters"
:key="filter.handle"
v-text="filter.title"
class="btn w-full mt-1"
@click="creating = filter.handle"
/>
</div>
<div v-else>
<data-list-filter
v-for="filter in unpinnedFilters"
v-if="creating === filter.handle"
:key="filter.handle"
:filter="filter"
:values="activeFilters[filter.handle]"
@changed="$emit('changed', {handle: filter.handle, values: $event})"
@cleared="creating = false"
@closed="closePopover"
/>
</div>
</div>
</template>
</popover>

<!-- Active filter badges -->
<div class="filter-badge mr-2 mb-2" v-for="(badge, handle) in fieldFilterBadges">
<span>{{ badge }}</span>
Expand DownExpand Up@@ -132,6 +158,28 @@ export default {
return this.filters.filter(filter => filter.handle !== 'fields');
},

pinnedFilters() {
return this.standardFilters.filter(filter => filter.pinned);
},

unpinnedFilters() {
return this.standardFilters.filter(filter => ! filter.pinned);
},

creatingFilter() {
return _.find(this.unpinnedFilters, filter => filter.handle === this.creating);
},

creatingFilterHeader() {
let text = data_get(this.creatingFilter, 'title', 'Filter where');

return __(text) + ':';
},

showUnpinnedFilterSelection() {
return ! this.creating;
},

fieldFilterBadges() {
return data_get(this.activeFilterBadges, 'fields', {});
},
Expand DownExpand Up@@ -207,11 +255,11 @@ export default {

delete fields[handle];

this.$emit('filter-changed', {handle: 'fields', values: fields});
this.$emit('changed', {handle: 'fields', values: fields});
},

removeStandardFilter(handle) {
this.$emit('filter-changed', {handle: handle, values: null});
this.$emit('changed', {handle: handle, values: null});
},

save() {
Expand All@@ -233,12 +281,6 @@ export default {
});
},

reset() {
return this.activePreset
? this.$emit('restore-preset', this.activePreset)
: this.$emit('reset');
},

remove() {
this.$preferences.remove(this.preferencesKey)
.then(response => {
Expand Down
Loading