MOBILE-4526 filter: Use get_all_states WS if available
parent
41e4292c48
commit
4fad121172
|
@ -15,7 +15,7 @@
|
||||||
import { Injectable, ViewContainerRef } from '@angular/core';
|
import { Injectable, ViewContainerRef } from '@angular/core';
|
||||||
|
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions } from './filter';
|
import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions, CoreFilterStateValue } from './filter';
|
||||||
import { CoreFilterDefaultHandler } from './handlers/default-filter';
|
import { CoreFilterDefaultHandler } from './handlers/default-filter';
|
||||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||||
import { CoreSite } from '@classes/sites/site';
|
import { CoreSite } from '@classes/sites/site';
|
||||||
|
@ -169,7 +169,7 @@ export class CoreFilterDelegateService extends CoreDelegate<CoreFilterHandler> {
|
||||||
filter: handler.filterName,
|
filter: handler.filterName,
|
||||||
inheritedstate: 1,
|
inheritedstate: 1,
|
||||||
instanceid: instanceId,
|
instanceid: instanceId,
|
||||||
localstate: 1,
|
localstate: CoreFilterStateValue.ON,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,7 +245,10 @@ export class CoreFilterDelegateService extends CoreDelegate<CoreFilterHandler> {
|
||||||
skipFilters?: string[],
|
skipFilters?: string[],
|
||||||
): boolean {
|
): boolean {
|
||||||
|
|
||||||
if (filter.localstate == -1 || (filter.localstate == 0 && filter.inheritedstate == -1)) {
|
if (
|
||||||
|
filter.localstate === CoreFilterStateValue.OFF ||
|
||||||
|
(filter.localstate === CoreFilterStateValue.INHERIT && filter.inheritedstate === CoreFilterStateValue.OFF)
|
||||||
|
) {
|
||||||
// Filter is disabled, ignore it.
|
// Filter is disabled, ignore it.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -255,7 +258,7 @@ export class CoreFilterDelegateService extends CoreDelegate<CoreFilterHandler> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skipFilters && skipFilters.indexOf(filter.filter) != -1) {
|
if (skipFilters && skipFilters.indexOf(filter.filter) !== -1) {
|
||||||
// Skip this filter.
|
// Skip this filter.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ import {
|
||||||
CoreFilterFormatTextOptions,
|
CoreFilterFormatTextOptions,
|
||||||
CoreFilterClassifiedFilters,
|
CoreFilterClassifiedFilters,
|
||||||
CoreFiltersGetAvailableInContextWSParamContext,
|
CoreFiltersGetAvailableInContextWSParamContext,
|
||||||
|
CoreFilterStateValue,
|
||||||
|
CoreFilterAllStates,
|
||||||
} from './filter';
|
} from './filter';
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CoreCourses } from '@features/courses/services/courses';
|
import { CoreCourses } from '@features/courses/services/courses';
|
||||||
|
@ -177,7 +179,7 @@ export class CoreFilterHelperProvider {
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<CoreFilterFilter[]> {
|
): Promise<CoreFilterFilter[]> {
|
||||||
// Check the right context to use.
|
// Check the right context to use.
|
||||||
const newContext = CoreFilter.convertContext(contextLevel, instanceId, { courseId: options.courseId });
|
const newContext = CoreFilter.getEffectiveContext(contextLevel, instanceId, { courseId: options.courseId });
|
||||||
contextLevel = newContext.contextLevel;
|
contextLevel = newContext.contextLevel;
|
||||||
instanceId = newContext.instanceId;
|
instanceId = newContext.instanceId;
|
||||||
|
|
||||||
|
@ -198,6 +200,11 @@ export class CoreFilterHelperProvider {
|
||||||
return await CoreFilterDelegate.getEnabledFilters(contextLevel, instanceId);
|
return await CoreFilterDelegate.getEnabledFilters(contextLevel, instanceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filters = await this.getFiltersInContextUsingAllStates(contextLevel, instanceId, options, site);
|
||||||
|
if (filters) {
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
const courseId = options.courseId;
|
const courseId = options.courseId;
|
||||||
let hasFilters = true;
|
let hasFilters = true;
|
||||||
|
|
||||||
|
@ -238,6 +245,95 @@ export class CoreFilterHelperProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filters in context using the all states data.
|
||||||
|
*
|
||||||
|
* @param contextLevel The context level.
|
||||||
|
* @param instanceId Instance ID related to the context.
|
||||||
|
* @param options Options.
|
||||||
|
* @param site Site.
|
||||||
|
* @returns Filters, undefined if all states cannot be used.
|
||||||
|
*/
|
||||||
|
protected async getFiltersInContextUsingAllStates(
|
||||||
|
contextLevel: ContextLevel,
|
||||||
|
instanceId: number,
|
||||||
|
options: CoreFilterFormatTextOptions = {},
|
||||||
|
site?: CoreSite,
|
||||||
|
): Promise<CoreFilterFilter[] | undefined> {
|
||||||
|
site = site || CoreSites.getCurrentSite();
|
||||||
|
|
||||||
|
if (!CoreFilter.canGetAllStatesInSite(site)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allStates = await CoreFilter.getAllStates({ siteId: site?.getId() });
|
||||||
|
if (
|
||||||
|
contextLevel !== ContextLevel.SYSTEM &&
|
||||||
|
contextLevel !== ContextLevel.COURSECAT &&
|
||||||
|
this.hasCategoryOverride(allStates)
|
||||||
|
) {
|
||||||
|
// A category has an override, we cannot calculate the right filters for child contexts.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contexts = CoreFilter.getContextsTreeList(contextLevel, instanceId, { courseId: options.courseId });
|
||||||
|
const contextId = Object.values(allStates[contextLevel]?.[instanceId] ?? {})[0]?.contextid;
|
||||||
|
|
||||||
|
const filters: Record<string, CoreFilterFilter> = {};
|
||||||
|
contexts.reverse().forEach((context) => {
|
||||||
|
const isParentContext = context.contextLevel !== contextLevel;
|
||||||
|
const filtersInContext = allStates[context.contextLevel]?.[context.instanceId];
|
||||||
|
if (!filtersInContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name in filtersInContext) {
|
||||||
|
const filterInContext = filtersInContext[name];
|
||||||
|
if (filterInContext.localstate === CoreFilterStateValue.DISABLED) {
|
||||||
|
// Ignore disabled filters to make it consistent with available in context.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
filters[name] = {
|
||||||
|
contextlevel: contextLevel,
|
||||||
|
instanceid: instanceId,
|
||||||
|
contextid: contextId,
|
||||||
|
filter: name,
|
||||||
|
localstate: isParentContext ? CoreFilterStateValue.INHERIT : filterInContext.localstate,
|
||||||
|
inheritedstate: isParentContext ?
|
||||||
|
filterInContext.localstate :
|
||||||
|
filters[name]?.inheritedstate ?? filterInContext.localstate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.values(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there is an override for a category in the states of all filters.
|
||||||
|
*
|
||||||
|
* @param states States to check.
|
||||||
|
* @returns True if has category override, false otherwise.
|
||||||
|
*/
|
||||||
|
protected hasCategoryOverride(states: CoreFilterAllStates): boolean {
|
||||||
|
if (!states[ContextLevel.COURSECAT]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const instanceId in states[ContextLevel.COURSECAT]) {
|
||||||
|
for (const name in states[ContextLevel.COURSECAT][instanceId]) {
|
||||||
|
if (
|
||||||
|
states[ContextLevel.COURSECAT][instanceId][name].localstate !== states[ContextLevel.SYSTEM][0][name].localstate
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get filters and format text.
|
* Get filters and format text.
|
||||||
*
|
*
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { CoreNetwork } from '@services/network';
|
import { CoreNetwork } from '@services/network';
|
||||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
|
||||||
import { CoreSite } from '@classes/sites/site';
|
import { CoreSite } from '@classes/sites/site';
|
||||||
import { CoreWSExternalWarning } from '@services/ws';
|
import { CoreWSExternalWarning } from '@services/ws';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
@ -62,11 +62,37 @@ export class CoreFilterProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if getting all states is available in site.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @returns Whether it's available.
|
||||||
|
* @since 4.4
|
||||||
|
*/
|
||||||
|
async canGetAllStates(siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
return this.canGetAllStatesInSite(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if getting all states is available in site.
|
||||||
|
*
|
||||||
|
* @param site Site. If not defined, current site.
|
||||||
|
* @returns Whether it's available.
|
||||||
|
* @since 4.4
|
||||||
|
*/
|
||||||
|
canGetAllStatesInSite(site?: CoreSite): boolean {
|
||||||
|
site = site || CoreSites.getCurrentSite();
|
||||||
|
|
||||||
|
return !!(site?.wsAvailable('core_filters_get_all_states'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether or not we can get the available filters: the WS is available and the feature isn't disabled.
|
* Returns whether or not we can get the available filters: the WS is available and the feature isn't disabled.
|
||||||
*
|
*
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved with boolean: whethe can get filters.
|
* @returns Whether can get filters.
|
||||||
*/
|
*/
|
||||||
async canGetFilters(siteId?: string): Promise<boolean> {
|
async canGetFilters(siteId?: string): Promise<boolean> {
|
||||||
const disabled = await this.checkFiltersDisabled(siteId);
|
const disabled = await this.checkFiltersDisabled(siteId);
|
||||||
|
@ -78,7 +104,7 @@ export class CoreFilterProvider {
|
||||||
* Returns whether or not we can get the available filters: the WS is available and the feature isn't disabled.
|
* Returns whether or not we can get the available filters: the WS is available and the feature isn't disabled.
|
||||||
*
|
*
|
||||||
* @param site Site. If not defined, current site.
|
* @param site Site. If not defined, current site.
|
||||||
* @returns Promise resolved with boolean: whethe can get filters.
|
* @returns Whether can get filters.
|
||||||
*/
|
*/
|
||||||
canGetFiltersInSite(site?: CoreSite): boolean {
|
canGetFiltersInSite(site?: CoreSite): boolean {
|
||||||
return !this.checkFiltersDisabledInSite(site);
|
return !this.checkFiltersDisabledInSite(site);
|
||||||
|
@ -153,7 +179,7 @@ export class CoreFilterProvider {
|
||||||
// Simulate the system context based on the inherited data.
|
// Simulate the system context based on the inherited data.
|
||||||
filter.contextlevel = ContextLevel.SYSTEM;
|
filter.contextlevel = ContextLevel.SYSTEM;
|
||||||
filter.instanceid = 0;
|
filter.instanceid = 0;
|
||||||
filter.contextid = -1;
|
filter.contextid = undefined;
|
||||||
filter.localstate = filter.inheritedstate;
|
filter.localstate = filter.inheritedstate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +197,7 @@ export class CoreFilterProvider {
|
||||||
* @param options Options.
|
* @param options Options.
|
||||||
* @returns Context to use.
|
* @returns Context to use.
|
||||||
*/
|
*/
|
||||||
convertContext(
|
getEffectiveContext(
|
||||||
contextLevel: ContextLevel,
|
contextLevel: ContextLevel,
|
||||||
instanceId: number,
|
instanceId: number,
|
||||||
options: {courseId?: number} = {},
|
options: {courseId?: number} = {},
|
||||||
|
@ -241,6 +267,53 @@ export class CoreFilterProvider {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for get all states WS call.
|
||||||
|
*
|
||||||
|
* @returns Cache key.
|
||||||
|
*/
|
||||||
|
protected getAllStatesCacheKey(): string {
|
||||||
|
return this.ROOT_CACHE_KEY + 'allStates';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the states for filters.
|
||||||
|
*
|
||||||
|
* @param options Options.
|
||||||
|
* @returns Promise resolved with the filters classified by context.
|
||||||
|
* @since 4.4
|
||||||
|
*/
|
||||||
|
async getAllStates(options: CoreSitesCommonWSOptions = {}): Promise<CoreFilterAllStates> {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getAllStatesCacheKey(),
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
|
// Use stale while revalidate by default, but always use the first value. If data is updated it will be stored in DB.
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy ?? CoreSitesReadingStrategy.STALE_WHILE_REVALIDATE),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await site.read<CoreFilterGetAllStatesWSResponse>('core_filters_get_all_states', {}, preSets);
|
||||||
|
|
||||||
|
const classified: CoreFilterAllStates = {};
|
||||||
|
|
||||||
|
result.filters.forEach((filter) => {
|
||||||
|
classified[filter.contextlevel] = classified[filter.contextlevel] || {};
|
||||||
|
classified[filter.contextlevel][filter.instanceid] = classified[filter.contextlevel][filter.instanceid] || {};
|
||||||
|
|
||||||
|
classified[filter.contextlevel][filter.instanceid][filter.filter] = {
|
||||||
|
contextlevel: filter.contextlevel,
|
||||||
|
instanceid: filter.instanceid,
|
||||||
|
contextid: filter.contextid,
|
||||||
|
filter: filter.filter,
|
||||||
|
localstate: filter.state,
|
||||||
|
inheritedstate: filter.state,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return classified;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cache key for available in contexts WS calls.
|
* Get cache key for available in contexts WS calls.
|
||||||
*
|
*
|
||||||
|
@ -370,6 +443,45 @@ export class CoreFilterProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a context, return the list of contexts used in the filters inheritance tree, from bottom to top.
|
||||||
|
* E.g. when using module, it will return the module context, course context (if course ID is supplied), category context
|
||||||
|
* (if categoy ID is supplied) and system context.
|
||||||
|
*
|
||||||
|
* @param contextLevel Context level.
|
||||||
|
* @param instanceId Instance ID.
|
||||||
|
* @param options Options
|
||||||
|
* @returns List of contexts.
|
||||||
|
*/
|
||||||
|
getContextsTreeList(
|
||||||
|
contextLevel: ContextLevel,
|
||||||
|
instanceId: number,
|
||||||
|
options: {courseId?: number; categoryId?: number} = {},
|
||||||
|
): { contextLevel: ContextLevel; instanceId: number }[] {
|
||||||
|
// Make sure context has been converted.
|
||||||
|
const newContext = CoreFilter.getEffectiveContext(contextLevel, instanceId, options);
|
||||||
|
contextLevel = newContext.contextLevel;
|
||||||
|
instanceId = newContext.instanceId;
|
||||||
|
|
||||||
|
const contexts = [
|
||||||
|
{ contextLevel, instanceId },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (contextLevel === ContextLevel.MODULE && options.courseId) {
|
||||||
|
contexts.push({ contextLevel: ContextLevel.COURSE, instanceId: options.courseId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((contextLevel === ContextLevel.MODULE || contextLevel === ContextLevel.COURSE) && options.categoryId) {
|
||||||
|
contexts.push({ contextLevel: ContextLevel.COURSECAT, instanceId: options.categoryId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextLevel !== ContextLevel.SYSTEM) {
|
||||||
|
contexts.push({ contextLevel: ContextLevel.SYSTEM, instanceId: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return contexts;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidates all available in context WS calls.
|
* Invalidates all available in context WS calls.
|
||||||
*
|
*
|
||||||
|
@ -382,6 +494,18 @@ export class CoreFilterProvider {
|
||||||
await site.invalidateWsCacheForKeyStartingWith(this.getAvailableInContextsPrefixCacheKey());
|
await site.invalidateWsCacheForKeyStartingWith(this.getAvailableInContextsPrefixCacheKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates get all states WS call.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID (empty for current site).
|
||||||
|
* @returns Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateAllStates(siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getAllStatesCacheKey());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidates available in context WS call.
|
* Invalidates available in context WS call.
|
||||||
*
|
*
|
||||||
|
@ -499,15 +623,15 @@ export type CoreFiltersGetAvailableInContextWSParamContext = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter object returned by core_filters_get_available_in_context.
|
* Filter data.
|
||||||
*/
|
*/
|
||||||
export type CoreFilterFilter = {
|
export type CoreFilterFilter = {
|
||||||
contextlevel: ContextLevel; // The context level where the filters are: (coursecat, course, module).
|
contextlevel: ContextLevel; // The context level where the filters are: (coursecat, course, module).
|
||||||
instanceid: number; // The instance id of item associated with the context.
|
instanceid: number; // The instance id of item associated with the context.
|
||||||
contextid: number; // The context id.
|
contextid?: number; // The context id. It will be undefined in cases where it cannot be calculated in the app.
|
||||||
filter: string; // Filter plugin name.
|
filter: string; // Filter plugin name.
|
||||||
localstate: number; // Filter state: 1 for on, -1 for off, 0 if inherit.
|
localstate: CoreFilterStateValue; // Filter state.
|
||||||
inheritedstate: number; // 1 or 0 to use when localstate is set to inherit.
|
inheritedstate: CoreFilterStateValue; // State to use when localstate is set to inherit.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -518,6 +642,36 @@ export type CoreFilterGetAvailableInContextResult = {
|
||||||
warnings: CoreWSExternalWarning[]; // List of warnings.
|
warnings: CoreWSExternalWarning[]; // List of warnings.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter state returned by core_filters_get_all_states.
|
||||||
|
*/
|
||||||
|
export type CoreFilterState = {
|
||||||
|
contextlevel: ContextLevel; // The context level where the filters are: (coursecat, course, module).
|
||||||
|
instanceid: number; // The instance id of item associated with the context.
|
||||||
|
contextid: number; // The context id.
|
||||||
|
filter: string; // Filter plugin name.
|
||||||
|
state: CoreFilterStateValue; // Filter state.
|
||||||
|
sortorder: number; // Sort order.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context levels enumeration.
|
||||||
|
*/
|
||||||
|
export const enum CoreFilterStateValue {
|
||||||
|
ON = 1,
|
||||||
|
INHERIT = 0,
|
||||||
|
OFF = -1,
|
||||||
|
DISABLED = -9999,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of core_filters_get_all_states.
|
||||||
|
*/
|
||||||
|
export type CoreFilterGetAllStatesWSResponse = {
|
||||||
|
filters: CoreFilterState[]; // Filter state.
|
||||||
|
warnings: CoreWSExternalWarning[]; // List of warnings.
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options that can be passed to format text.
|
* Options that can be passed to format text.
|
||||||
*/
|
*/
|
||||||
|
@ -541,3 +695,14 @@ export type CoreFilterClassifiedFilters = {
|
||||||
[instanceid: number]: CoreFilterFilter[];
|
[instanceid: number]: CoreFilterFilter[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All filter states classified by context, instance and filter name.
|
||||||
|
*/
|
||||||
|
export type CoreFilterAllStates = {
|
||||||
|
[contextlevel: string]: {
|
||||||
|
[instanceid: number]: {
|
||||||
|
[filtername: string]: CoreFilterFilter;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue