-
-
Notifications
You must be signed in to change notification settings - Fork 23
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
Add more advanced series filtering mechanism #485
Labels
enhancement
New feature or request
needs-merged
Issue has been resolved on a development branch
web-ui
Part of the v2.0 Web Interface
Comments
Working Implementation<template id="filter-template">
<div class="three fields">
<div class="field" data-label="field">
<label>Field</label>
<div class="ui selection dropdown">
<input type="hidden" name="field" onchange="updateConditions(this);">
<i class="dropdown icon"></i>
<div class="default text">Field Name</div>
<div class="menu"></div>
</div>
</div>
<div class="field" data-label="condition">
<label>Condition</label>
<div class="ui selection dropdown">
<input type="hidden" name="condition">
<i class="dropdown icon"></i>
<div class="default text">Condition Type</div>
<div class="menu"></div>
</div>
</div>
<div class="disabled field" data-label="reference">
<label>Reference</label>
<input type="text" name="reference" placeholder="Reference Value">
</div>
</div>
</template>
<div class="ui modal">
<div class="header">Filter Settings</div>
<div class="content">
<form class="ui form">
<div class="ui field">
<label>Filter Name</label>
<input type="text" placeholder="Name" value="My Filter">
</div>
<div class="ui divider"></div>
<!-- Conditions added later -->
</form>
<div class="ui labeled icon button" onclick="addFilterCondition();">
<i class="filter icon"></i>
Add Filter
</div>
</div>
<div class="actions">
<button class="ui button" onclick="serializeForm();">
Apply Filter
</button>
</div>
</div> // Populate filters
const filterSettings = [
// name value type
['Series Name', 'name', 'string' ],
['Series Year', 'year', 'numeric' ],
['Monitored Status', 'monitored', 'boolean' ],
['Series ID', 'id', 'numeric' ],
['Sync ID', 'sync_id', 'nullable numeric'],
['Font ID', 'font_id', 'nullable numeric'],
['Card Directory', 'card_directory', 'nullable string' ],
['List of Libraries', 'libraries', 'list' ],
['Has Missing Title Cards', 'missing_cards', 'boolean', ],
].map(setting => {
return { name: setting[0], value: setting[1], type: setting[2] };
});
const filterChoices = {
'string': [
{name: 'equals', requiresInput: true},
{name: 'does not equal', requiresInput: true},
{name: 'contains', requiresInput: true},
{name: 'does not contain', requiresInput: true},
{name: 'starts with', requiresInput: true},
{name: 'does not start with', requiresInput: true},
{name: 'ends with', requiresInput: true},
{name: 'does not end with', requiresInput: true},
{name: 'matches', requiresInput: true},
{name: 'does not match', requiresInput: true},
],
'nullable string': [
{name: 'equals', requiresInput: true },
{name: 'does not equal', requiresInput: true },
{name: 'contains', requiresInput: true },
{name: 'does not contain', requiresInput: true },
{name: 'starts with', requiresInput: true },
{name: 'does not start with', requiresInput: true },
{name: 'ends with', requiresInput: true },
{name: 'does not end with', requiresInput: true },
{name: 'matches', requiresInput: true },
{name: 'does not match', requiresInput: true },
{name: 'is null', requiresInput: false},
{name: 'is not null', requiresInput: false},
],
'numeric': [
{name: 'is less than', requiresInput: true},
{name: 'is less than or equal to', requiresInput: true},
{name: 'equals', requiresInput: true},
{name: 'is greater than', requiresInput: true},
{name: 'is greater than or equal to', requiresInput: true},
],
'nullable numeric': [
{name: 'is less than', requiresInput: true },
{name: 'is less than or equal to', requiresInput: true },
{name: 'equals', requiresInput: true },
{name: 'is greater than', requiresInput: true },
{name: 'is greater than or equal to', requiresInput: true },
{name: 'is null', requiresInput: false},
{name: 'is not null', requiresInput: false},
],
'boolean': [
{name: 'is true', requiresInput: false},
{name: 'is false', requiresInput: false},
],
'nullable boolean': [
{name: 'is true', requiresInput: false},
{name: 'is false', requiresInput: false},
{name: 'is null', requiresInput: false},
{name: 'is not null', requiresInput: false},
],
'list': [
{name: 'is empty', requiresInput: false},
{name: 'is not empty', requiresInput: false},
{name: 'includes', requiresInput: true },
{name: 'does not include', requiresInput: true },
],
};
function updateConditions(obj) {
// Get the type of the newly selected filter field
// const fieldType = filterSettings.filter(fltr => fltr.value === obj.value)[0].type;
const fieldType = filterSettings.find(filter => filter.value === obj.value).type;
// Initialize associated dropdown with condition choices for this type
$(obj).closest('.fields').find('[data-label="condition"] .dropdown').dropdown({
onChange: (condition, text, $selectedItem) => {
// Enable/disable the dropdown based on the selected condition
const requiresInput = filterChoices[fieldType].find(choice => choice.name === condition).requiresInput;
$($selectedItem).closest('.fields').find('.field[data-label="reference"]').toggleClass('disabled', !requiresInput);
},
values: filterChoices[fieldType].map(choice => {
return {
name: choice.name,
value: choice.name,
selected: false,
};
}),
});
// Disable and clear reference field until condition is selected
const referenceField = $(obj).closest('.fields').find('.field[data-label="reference"]');
referenceField.toggleClass('disabled', true);
referenceField.find('input').val('');
}
const template = document.getElementById('filter-template').content;
filterSettings.forEach(filter => {
const item = document.createElement('div');
item.className = 'item'; item.dataset.value = filter.value; item.innerText = filter.name;
template.querySelector('.dropdown .menu').appendChild(item);
});
function addFilterCondition() {
document.querySelector('.modal .form').appendChild(template.cloneNode(true));
$('.dropdown').dropdown();
}
$('.modal').modal({blurring: true}).modal('show');
// Function to serialize form inputs into a list of objects
function serializeForm() {
const data = [];
// Loop through each group of fields and gather inputs
document.querySelectorAll('.modal .fields').forEach((fieldDiv) => {
// Get the values of 'name', 'condition', and 'reference' inputs
const nameInput = fieldDiv.querySelector('input[name="field"]');
const conditionInput = fieldDiv.querySelector('input[name="condition"]');
const referenceInput = fieldDiv.querySelector('input[name="reference"]');
const requiresInput = !fieldDiv.querySelector('.field[data-label="reference"]').className.includes('disabled');
// Ensure all inputs exist and retrieve their values
if (nameInput && conditionInput && referenceInput) {
const name = nameInput.value,
condition = conditionInput.value,
reference = referenceInput.value || null;
if (name && condition && (reference || !requiresInput)) {
data.push({
name: name,
condition: condition,
reference: reference
});
}
}
});
console.log(data);
return data;
} |
More Progress..HTML Templates<template id="blank-tab-template">
<div class="ui bottom attached tab segment">
<form class="ui form">
<div class="ui field">
<label>Filter Name</label>
<input type="text" name="filter_name" placeholder="Name" value="New Filter" oninput="updateTitle(this);">
</div>
<div class="ui divider"></div>
<!-- Conditions added later -->
</form>
<div class="ui labeled icon button" onclick="addFilterCondition(this);">
<i class="plus circle icon"></i>
Add Condition
</div>
<div class="ui right floated red icon button" onclick="deleteFilter(this);">
<i class="trash alternate outline icon"></i>
Delete Filter
</div>
</div>
</template>
<template id="filter-template">
<div class="three fields">
<div class="field" data-label="field">
<label>Field</label>
<div class="ui selection clearable dropdown">
<input type="hidden" name="field" onchange="updateConditions(this);">
<i class="dropdown icon"></i>
<div class="default text">Field Name</div>
<div class="menu"></div>
</div>
</div>
<div class="field" data-label="condition">
<label>Condition</label>
<div class="ui selection dropdown">
<input type="hidden" name="condition">
<i class="dropdown icon"></i>
<div class="default text">Condition Type</div>
<div class="menu"></div>
</div>
</div>
<div class="disabled field" data-label="reference">
<label>Reference</label>
<input type="text" name="reference" placeholder="Reference Value">
</div>
</div>
</template> HTML Modal<div class="ui modal">
<div class="header">Filter Settings</div>
<div class="content">
<div class="ui top attached tabular wrapping menu">
<!-- Tab selector added later -->
<div class="item add-tab" data-tab="add" onclick="addTab();"><i class="plus circle blue icon"></i></div>
</div>
<!-- Tabs added later -->
</div>
<div class="actions">
<button class="ui blue button" onclick="serializeAllFilters();">
<i class="check icon"></i>
Apply Filter
</button>
</div>
</div> JavaScript<script type="text/javascript">
/**
* "Live" update the title of the tab containing this filter name input.
**/
function updateTitle(inputElement) {
const tabName = inputElement.closest('.tab.segment').dataset.tab;
document.querySelector(`.modal .tabular.menu .item[data-tab="${tabName}"]`).innerText = inputElement.value;
}
// Add a new tab to the filter modal
function addTab(filter=null) {
// Determine tab number - check for existence in case a middle tab was deleted
let tabNumber = document.querySelectorAll('.modal .tabular.menu .item').length - 1;
while (document.querySelector(`.modal .tabular.menu .item[data-tab="tab${tabNumber}"]`)) {
tabNumber += 1;
}
// Add new tab selector to menu, just before add tab item
const $tabHeader = $('<div>', {
class: 'item',
'data-tab': 'tab' + tabNumber,
text: filter?.name || 'New Filter',
});
$('.modal .tabular.menu .item.add-tab').before($tabHeader);
// Add blank tab
const newTab = document.getElementById('blank-tab-template').content.cloneNode(true);
newTab.querySelector('.tab').dataset.tab = 'tab' + tabNumber;
if (filter) {
newTab.querySelector('input[name="filter_name"]').value = filter.name;
}
document.querySelector('.modal .content').appendChild(newTab);
$('.modal .tabular.menu .item').tab();
const tabs = document.querySelectorAll('.modal .content .tab.segment');
return tabs[tabs.length - 1];
}
function deleteFilter(deleteButton) {
const tabID = deleteButton.closest('.tab.segment').dataset.tab;
document.querySelectorAll(`.modal [data-tab="${tabID}"]`).forEach(tab => tab.remove());
$('.modal .tabular.menu .item').tab('change tab', 'tab0');
}
</script>
<script type="text/javascript">
const filterSettings = [
// name value type
['Card Directory', 'directory', 'nullable string' ],
['Series Name', 'name', 'string' ],
['Series Year', 'year', 'numeric' ],
['Monitored Status', 'monitored', 'boolean' ],
['Series ID', 'id', 'numeric' ],
['Sync ID', 'sync_id', 'nullable numeric'],
['Font ID', 'font_id', 'nullable numeric'],
['Episode Data Source ID', 'data_source_id', 'nullable numeric'],
['List of Libraries', 'libraries', 'list' ],
['Has Missing Title Cards', 'missing_cards', 'boolean', ],
['Card Filename Format', 'card_filename_format', 'nullable string' ],
['Enable Specials', 'sync_specials', 'nullable boolean'],
['Localized Image Rejection', 'skip_localized_images', 'nullable boolean'],
['List of Translations', 'translations', 'list' ],
['Match Titles', 'match_titles', 'boolean' ],
['Auto-Split Titles', 'auto_split_titles', 'boolean' ],
['Per-Season Assets', 'use_per_season_assets', 'boolean' ],
['Image Source Priority', 'image_source_priority', 'list' ],
['Emby Database ID', 'emby_id', 'nullable string' ],
['IMDb Database ID', 'imdb_id', 'nullable string' ],
['Jellyfin Database ID', 'jellyfin_id', 'nullable string' ],
['Sonarr Database ID', 'sonarr_id', 'nullable string' ],
['TMDb Database ID', 'tmdb_id', 'nullable numeric'],
['TVDb Database ID', 'tvdb_id', 'nullable numeric'],
['TVRage Database ID', 'tvrage_id', 'nullable string' ],
['Font Color', 'font_color', 'nullable string' ],
['Font Title Case', 'font_title_case', 'nullable string' ],
['Font Size', 'font_size', 'nullable numeric'],
['Font Kerning', 'font_kerning', 'nullable numeric'],
['Font Stroke Width', 'font_stroke_width', 'nullable numeric'],
['Font Interline Spacing', 'font_interline_spacing', 'nullable numeric'],
['Font Interword Spacing', 'font_interword_spacing', 'nullable numeric'],
['Font Vertical Shift', 'font_vertical_shift', 'nullable numeric'],
['Card Type', 'card_type', 'nullable string' ],
['Hide Season Text', 'hide_season_text', 'nullable boolean'],
['Season Title List', 'season_titles', 'nullable list' ],
['Hide Episode Text', 'hide_episode_text', 'nullable boolean'],
['Episode Text Format', 'episode_text_format', 'nullable string' ],
['Unwatched Card Style', 'unwatched_style', 'nullable string' ],
['Watched Card Style', 'watched_style', 'nullable string' ],
['Extras', 'extras', 'nullable list' ],
].map(setting => {
return { name: setting[0], value: setting[1], type: setting[2] };
});
const filterChoices = {
'string': [
{name: 'equals', requiresInput: true},
{name: 'does not equal', requiresInput: true},
{name: 'contains', requiresInput: true},
{name: 'does not contain', requiresInput: true},
{name: 'starts with', requiresInput: true},
{name: 'does not start with', requiresInput: true},
{name: 'ends with', requiresInput: true},
{name: 'does not end with', requiresInput: true},
{name: 'matches', requiresInput: true},
{name: 'does not match', requiresInput: true},
],
'nullable string': [
{name: 'equals', requiresInput: true },
{name: 'does not equal', requiresInput: true },
{name: 'contains', requiresInput: true },
{name: 'does not contain', requiresInput: true },
{name: 'starts with', requiresInput: true },
{name: 'does not start with', requiresInput: true },
{name: 'ends with', requiresInput: true },
{name: 'does not end with', requiresInput: true },
{name: 'matches', requiresInput: true },
{name: 'does not match', requiresInput: true },
{name: 'is null', requiresInput: false},
{name: 'is not null', requiresInput: false},
],
'numeric': [
{name: 'is less than', requiresInput: true},
{name: 'is less than or equal to', requiresInput: true},
{name: 'equals', requiresInput: true},
{name: 'is greater than', requiresInput: true},
{name: 'is greater than or equal to', requiresInput: true},
],
'nullable numeric': [
{name: 'is less than', requiresInput: true },
{name: 'is less than or equal to', requiresInput: true },
{name: 'equals', requiresInput: true },
{name: 'is greater than', requiresInput: true },
{name: 'is greater than or equal to', requiresInput: true },
{name: 'is null', requiresInput: false},
{name: 'is not null', requiresInput: false},
],
'boolean': [
{name: 'is true', requiresInput: false},
{name: 'is false', requiresInput: false},
],
'nullable boolean': [
{name: 'is true', requiresInput: false},
{name: 'is false', requiresInput: false},
{name: 'is null', requiresInput: false},
{name: 'is not null', requiresInput: false},
],
'list': [
{name: 'is empty', requiresInput: false},
{name: 'is not empty', requiresInput: false},
{name: 'includes', requiresInput: true },
{name: 'does not include', requiresInput: true },
],
'nullable list': [
{name: 'is empty', requiresInput: false},
{name: 'is not empty', requiresInput: false},
{name: 'includes', requiresInput: true },
{name: 'does not include', requiresInput: true },
{name: 'is null', requiresInput: false},
{name: 'is not null', requiresInput: false},
],
};
function updateConditions(obj) {
// Disable and clear reference field until condition is selected
const referenceField = $(obj).closest('.fields').find('.field[data-label="reference"]');
referenceField.toggleClass('disabled', true);
referenceField.find('input').val('');
// No value means the field was cleared
if (!obj.value) {
// Clear condition dropdown
$(obj).closest('.fields').find('[data-label="condition"] .ui.dropdown').dropdown({
values: []
});
return;
}
// Get the type of the newly selected filter field
const fieldType = filterSettings.find(filter => filter.value === obj.value).type;
// Initialize associated dropdown with condition choices for this type
$(obj).closest('.fields').find('[data-label="condition"] .ui.dropdown').dropdown({
onChange: (condition, text, $selectedItem) => {
// Enable/disable the dropdown based on the selected condition
const requiresInput = filterChoices[fieldType].find(choice => choice.name === condition).requiresInput;
$($selectedItem).closest('.fields').find('.field[data-label="reference"]').toggleClass('disabled', !requiresInput);
},
placeholder: 'Condition Type',
values: filterChoices[fieldType].map(choice => {
return {
name: choice.name,
value: choice.name,
selected: false,
};
}),
});
}
const template = document.getElementById('filter-template').content;
filterSettings.forEach(filter => {
const item = document.createElement('div');
item.className = 'item'; item.dataset.value = filter.value; item.innerText = filter.name;
template.querySelector('.dropdown .menu').appendChild(item);
});
function addFilterCondition(addButton, removeLabels=true) {
const newFields = document.getElementById('filter-template').content.cloneNode(true);
if (removeLabels) {
newFields.querySelectorAll('.field label').forEach(label => label.remove());
}
// Add to page, initialize dropdowns
addButton.closest('.tab.segment').querySelector('.form').appendChild(newFields);
// document.querySelector('.modal .form').appendChild(newFields);
$('.modal .form .dropdown').dropdown();
}
$('.modal').modal({blurring: true}).modal('show');
</script>
<script type="text/javascript">
// Function to serialize form inputs into a list of objects
function serializeAllFilters() {
const filters = [];
let activeTab = null;
// Serialize each tab
document.querySelectorAll('.modal .content .tab.segment').forEach((tab, index) => {
const data = {
name: tab.querySelector('input[name="filter_name"]').value,
conditions: [],
};
// Update active tab number if needed
if (activeTab === null && tab.classList.contains('active')) {
activeTab = index;
}
// Loop through each group of fields and gather inputs
tab.querySelectorAll('.fields').forEach((fieldDiv) => {
// Get the values of all input fields
const fieldInput = fieldDiv.querySelector('input[name="field"]');
const conditionInput = fieldDiv.querySelector('input[name="condition"]');
const referenceInput = fieldDiv.querySelector('input[name="reference"]');
// An input is required if the reference field is not disabled
const requiresInput = !fieldDiv.querySelector('.field[data-label="reference"]').className.includes('disabled');
// Ensure all inputs exist and retrieve their values
if (fieldInput && conditionInput && referenceInput) {
const field = fieldInput.value,
condition = conditionInput.value,
reference = referenceInput.value || null;
if (field && condition && (reference || !requiresInput)) {
data.conditions.push({ field, condition, reference, });
}
}
});
filters.push(data);
});
console.log({ filters, activeTab });
return { filters, activeTab };
}
</script>
<script type="text/javascript">
// Eventually will be passed via Jinja or AJAX call
const existingFilters = [
{
name: 'Missing Cards (no Star)',
conditions: [
{ name: "name", condition: "does not contain", reference: "Star" },
{ name: "missing_cards", condition: "is true", reference: null },
],
},
{
name: 'Unmonitored and Missing Cards',
conditions: [
{ name: "monitored", condition: "is false", reference: null },
{ name: "missing_cards", condition: "is true", reference: null },
]
}
];
existingFilters.forEach(filter => {
// Add blank tab for this filter
const tab = addTab(filter);
// Add each condition
filter.conditions.forEach((condition, index) => {
// Add new filter row for this condition
const newFields = document.getElementById('filter-template').content.cloneNode(true);
// Remove labels for all but the first condition
if (index > 0) {
newFields.querySelectorAll('.field label').forEach(label => label.remove());
}
// Add to page so dropdowns can be populated
tab.querySelector('.form').appendChild(newFields);
// Populate fields
$(tab).find('.field[data-label="field"] div.dropdown').last().dropdown('set selected', condition.name);
$(tab).find('.field[data-label="condition"] div.dropdown').last().dropdown('set selected', condition.condition);
if (condition.reference !== null) {
$(tab).find('.field[data-label="reference"] input').last().val(condition.reference);
}
});
});
</script> |
CollinHeist
added
enhancement
New feature or request
needs-merged
Issue has been resolved on a development branch
web-ui
Part of the v2.0 Web Interface
labels
Nov 15, 2024
CollinHeist
added a commit
that referenced
this issue
Nov 15, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
enhancement
New feature or request
needs-merged
Issue has been resolved on a development branch
web-ui
Part of the v2.0 Web Interface
Allow more complex filters based on all available Series fields.
id
name
year
monitored
data_source_id
font_id
sync_id
directory
card_filename_format
sync_specials
skip_localized_images
The filter conditions should probably be based on the data type of the attribute, so:
The text was updated successfully, but these errors were encountered: