Improved our conditional filter
This commit is contained in:
@@ -52,7 +52,7 @@
|
||||
function filtersAsJSON() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
version: 'v1',
|
||||
version: 'v2',
|
||||
createdFor: 'materialious',
|
||||
filterBy: contentFilters
|
||||
},
|
||||
@@ -98,6 +98,14 @@
|
||||
|
||||
<article class="error-container">
|
||||
<p>{$_('layout.backendEngine.warning')}</p>
|
||||
<p>
|
||||
Need Help? Check out our <a
|
||||
class="link"
|
||||
href="https://github.com/Materialious/Materialious/blob/main/docs/CONTENT-FILTERS.md"
|
||||
target="_blank"
|
||||
referrerpolicy="no-referrer">guide</a
|
||||
>.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<form onsubmit={loadFilterList}>
|
||||
@@ -179,9 +187,9 @@
|
||||
<hr />
|
||||
{#if filter.conditions}
|
||||
<ul class="list">
|
||||
{#each filter.conditions as condition (condition)}
|
||||
{#each filter.conditions as condition, conditionIndex (condition)}
|
||||
<li style="display: block;">
|
||||
<nav class="right-align no-margin">
|
||||
<nav class="no-margin">
|
||||
<button
|
||||
onclick={() => {
|
||||
filter.conditions = filter.conditions.filter((item) => condition !== item);
|
||||
@@ -191,6 +199,7 @@
|
||||
class="surface-container-highest"
|
||||
>
|
||||
<i>close</i>
|
||||
<span>Delete conditional</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@@ -234,66 +243,102 @@
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
|
||||
{#if schema[filter.type][condition.field] === 'boolean'}
|
||||
<div class="field label suffix surface-container-highest">
|
||||
<select
|
||||
onchange={(event: Event & { currentTarget: HTMLSelectElement }) => {
|
||||
condition.value = event.currentTarget.value;
|
||||
filterContentListStore.set(contentFilters);
|
||||
filterContentUrlAutoUpdateStore.set(false);
|
||||
}}
|
||||
name="boolean-options"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>{$_('layout.filter.optionPlaceholder')}</option
|
||||
>
|
||||
<option selected={condition.value === 'true'} value="true">true</option>
|
||||
<option selected={condition.value === 'false'} value="false">false</option>
|
||||
</select>
|
||||
<label for="boolean-options">Value</label>
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
{:else if Array.isArray(schema[filter.type][condition.field])}
|
||||
<div class="field label suffix surface-container-highest">
|
||||
<select
|
||||
onchange={(event: Event & { currentTarget: HTMLSelectElement }) => {
|
||||
condition.value = event.currentTarget.value;
|
||||
filterContentListStore.set(contentFilters);
|
||||
filterContentUrlAutoUpdateStore.set(false);
|
||||
}}
|
||||
name="array-options"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>{$_('layout.filter.optionPlaceholder')}</option
|
||||
>
|
||||
{#each schema[filter.type][condition.field] as value (value)}
|
||||
<option selected={condition.value === value} {value}
|
||||
>{camelCaseToHuman(value)}</option
|
||||
{#each condition.values as conditionValue, index (index)}
|
||||
<nav>
|
||||
{#if schema[filter.type][condition.field] === 'boolean'}
|
||||
<div class="field label suffix surface-container-highest max">
|
||||
<select
|
||||
onchange={(event: Event & { currentTarget: HTMLSelectElement }) => {
|
||||
condition.values[index] = event.currentTarget.value;
|
||||
filterContentListStore.set(contentFilters);
|
||||
filterContentUrlAutoUpdateStore.set(false);
|
||||
}}
|
||||
name="boolean-options"
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
<label for="array-options">Value</label>
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="field label border">
|
||||
<input
|
||||
oninput={(event: Event & { currentTarget: HTMLInputElement }) => {
|
||||
condition.value =
|
||||
schema[filter.type][condition.field] === 'number'
|
||||
? Number(event.currentTarget.value)
|
||||
: event.currentTarget.value;
|
||||
<option value="" disabled selected
|
||||
>{$_('layout.filter.optionPlaceholder')}</option
|
||||
>
|
||||
<option selected={conditionValue === 'true'} value="true">true</option>
|
||||
<option selected={conditionValue === 'false'} value="false">false</option>
|
||||
</select>
|
||||
<label for="boolean-options">Value</label>
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
{:else if Array.isArray(schema[filter.type][condition.field])}
|
||||
<div class="field label suffix surface-container-highest max">
|
||||
<select
|
||||
onchange={(event: Event & { currentTarget: HTMLSelectElement }) => {
|
||||
condition.values[index] = event.currentTarget.value;
|
||||
filterContentListStore.set(contentFilters);
|
||||
filterContentUrlAutoUpdateStore.set(false);
|
||||
}}
|
||||
name="array-options"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>{$_('layout.filter.optionPlaceholder')}</option
|
||||
>
|
||||
{#each schema[filter.type][condition.field] as value (value)}
|
||||
<option selected={conditionValue === value} {value}
|
||||
>{camelCaseToHuman(value)}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
<label for="array-options">Value</label>
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="field label surface-container-highest max">
|
||||
<input
|
||||
oninput={(event: Event & { currentTarget: HTMLInputElement }) => {
|
||||
condition.values[index] =
|
||||
schema[filter.type][condition.field] === 'number'
|
||||
? Number(event.currentTarget.value)
|
||||
: event.currentTarget.value;
|
||||
filterContentListStore.set(contentFilters);
|
||||
filterContentUrlAutoUpdateStore.set(false);
|
||||
}}
|
||||
name="value"
|
||||
type={schema[filter.type][condition.field] === 'string' ? 'text' : 'number'}
|
||||
value={conditionValue}
|
||||
/>
|
||||
<label for="value">{$_('layout.filter.value')}</label>
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => {
|
||||
condition.values = condition.values.filter((item) => conditionValue !== item);
|
||||
filterContentListStore.set(contentFilters);
|
||||
filterContentUrlAutoUpdateStore.set(false);
|
||||
}}
|
||||
name="value"
|
||||
type={schema[filter.type][condition.field] === 'string' ? 'text' : 'number'}
|
||||
value={condition.value}
|
||||
/>
|
||||
<label for="value">{$_('layout.filter.value')}</label>
|
||||
</div>
|
||||
{/if}
|
||||
class="circle surface-container-highest"
|
||||
>
|
||||
<i>close</i>
|
||||
<div class="tooltip">Delete Value</div>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{#if condition.values.length > 0 && index < condition.values.length - 1}
|
||||
<h6 class="center-align">OR</h6>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<div class="space"></div>
|
||||
<button
|
||||
onclick={() => {
|
||||
condition.values.push('');
|
||||
filterContentListStore.set(contentFilters);
|
||||
filterContentUrlAutoUpdateStore.set(false);
|
||||
}}
|
||||
class="surface-container-highest"
|
||||
>
|
||||
<i>add</i>
|
||||
<span>Add OR</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{#if filter.conditions.length > 0 && conditionIndex < filter.conditions.length - 1}
|
||||
<h6 class="center-align">AND</h6>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
@@ -303,8 +348,9 @@
|
||||
filter.conditions.push({
|
||||
operator: 'equals',
|
||||
field: 'author',
|
||||
value: ''
|
||||
values: ['']
|
||||
});
|
||||
filterContentListStore.set(contentFilters);
|
||||
filterContentUrlAutoUpdateStore.set(false);
|
||||
}}
|
||||
class="surface-container-highest"
|
||||
|
||||
@@ -23,14 +23,17 @@ const zFilterCondition = z.object({
|
||||
message: 'Invalid field'
|
||||
}), // Field to filter
|
||||
operator: zFilterOperatorEnum, // Operator
|
||||
value: z.union([
|
||||
// Value to compare against
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.array(z.string()),
|
||||
z.array(z.number()),
|
||||
z.string().regex(/.*/)
|
||||
])
|
||||
values: z.array(
|
||||
z.union([
|
||||
// Value to compare against
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.array(z.string()),
|
||||
z.array(z.number()),
|
||||
z.string().regex(/.*/)
|
||||
])
|
||||
),
|
||||
note: z.string().optional()
|
||||
});
|
||||
|
||||
// Logical grouping operator
|
||||
@@ -42,7 +45,7 @@ export const zFilterGroup = z.object({
|
||||
export const zFilterSchema = z.array(zFilterGroup);
|
||||
|
||||
const zFilterRootSchema = z.object({
|
||||
version: z.literal('v1'),
|
||||
version: z.literal('v2'),
|
||||
createdFor: z.literal('materialious'),
|
||||
filterBy: zFilterSchema
|
||||
});
|
||||
@@ -66,45 +69,47 @@ export function isItemFiltered(item: FeedItem): boolean {
|
||||
|
||||
const fieldValue = item[condition.field as keyof FeedItem];
|
||||
|
||||
switch (condition.operator) {
|
||||
case 'equals':
|
||||
return fieldValue.toString() === condition.value.toString();
|
||||
return condition.values.some((conditionValue) => {
|
||||
switch (condition.operator) {
|
||||
case 'equals':
|
||||
return fieldValue.toString() === conditionValue.toString();
|
||||
|
||||
case 'in':
|
||||
return Array.isArray(condition.value) && (condition.value as any[]).includes(fieldValue);
|
||||
case 'in':
|
||||
return Array.isArray(conditionValue) && (conditionValue as any[]).includes(fieldValue);
|
||||
|
||||
case 'like':
|
||||
return (
|
||||
typeof fieldValue === 'string' &&
|
||||
typeof condition.value === 'string' &&
|
||||
fieldValue.toLowerCase().includes(condition.value.toLowerCase())
|
||||
);
|
||||
case 'like':
|
||||
return (
|
||||
typeof fieldValue === 'string' &&
|
||||
typeof conditionValue === 'string' &&
|
||||
fieldValue.toLowerCase().includes(conditionValue.toLowerCase())
|
||||
);
|
||||
|
||||
case 'gt':
|
||||
return (
|
||||
typeof fieldValue === 'number' &&
|
||||
typeof condition.value === 'number' &&
|
||||
fieldValue > condition.value
|
||||
);
|
||||
case 'gt':
|
||||
return (
|
||||
typeof fieldValue === 'number' &&
|
||||
typeof conditionValue === 'number' &&
|
||||
fieldValue > conditionValue
|
||||
);
|
||||
|
||||
case 'lt':
|
||||
return (
|
||||
typeof fieldValue === 'number' &&
|
||||
typeof condition.value === 'number' &&
|
||||
fieldValue < condition.value
|
||||
);
|
||||
case 'lt':
|
||||
return (
|
||||
typeof fieldValue === 'number' &&
|
||||
typeof conditionValue === 'number' &&
|
||||
fieldValue < conditionValue
|
||||
);
|
||||
|
||||
case 'regex':
|
||||
if (typeof condition.value !== 'string' || !isSafeRegex(condition.value)) return false;
|
||||
case 'regex':
|
||||
if (typeof conditionValue !== 'string' || !isSafeRegex(conditionValue)) return false;
|
||||
|
||||
return typeof fieldValue === 'string' && new RegExp(condition.value).test(fieldValue);
|
||||
return typeof fieldValue === 'string' && new RegExp(conditionValue).test(fieldValue);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return filterGroup.conditions.every(evaluateCondition);
|
||||
return filterGroup.conditions.some(evaluateCondition);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ export const VideoSchema: SchemaStructure = {
|
||||
type: ['video', 'shortVideo', 'stream'],
|
||||
videoId: 'string',
|
||||
viewCount: 'number',
|
||||
viewCountText: 'string'
|
||||
viewCountText: 'string',
|
||||
lengthSeconds: 'number',
|
||||
premiereTimestamp: 'number',
|
||||
isUpcoming: 'boolean',
|
||||
premium: 'boolean'
|
||||
};
|
||||
|
||||
export const ChannelSchema: SchemaStructure = {
|
||||
|
||||
Reference in New Issue
Block a user