mirror of
https://github.com/OCA/web.git
synced 2025-02-22 13:21:25 +02:00
[MIG] web_responsive: Migration to 17.0
This commit is contained in:
@@ -1,45 +1,73 @@
|
||||
/** @odoo-module **/
|
||||
/* Copyright 2021 ITerra - Sergey Shebanin
|
||||
* Copyright 2023 Onestein - Anjeel Haria
|
||||
/* Copyright 2023 Taras Shabaranskyi
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
|
||||
|
||||
import LegacyControlPanel from "web.ControlPanel";
|
||||
import {ControlPanel} from "@web/search/control_panel/control_panel";
|
||||
import {deviceContext} from "@web_responsive/components/ui_context.esm";
|
||||
import {patch} from "web.utils";
|
||||
import {Dropdown} from "@web/core/dropdown/dropdown";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {browser} from "@web/core/browser/browser";
|
||||
|
||||
const {useState} = owl;
|
||||
export const STICKY_CLASS = "o_mobile_sticky";
|
||||
|
||||
// In v15.0 there are two ControlPanel's. They are mostly the same and are used in legacy and new owl views.
|
||||
// We extend them two mostly the same way.
|
||||
/**
|
||||
* @param {Number} delay
|
||||
* @returns {{collect: function(Number, (function(Number, Number): void)): void}}
|
||||
*/
|
||||
export function minMaxCollector(delay = 100) {
|
||||
const state = {
|
||||
id: null,
|
||||
items: [],
|
||||
};
|
||||
|
||||
// Patch legacy control panel to add states for mobile quick search
|
||||
patch(LegacyControlPanel.prototype, "web_responsive.LegacyControlPanelMobile", {
|
||||
function min() {
|
||||
return Math.min.apply(null, state.items);
|
||||
}
|
||||
|
||||
function max() {
|
||||
return Math.max.apply(null, state.items);
|
||||
}
|
||||
|
||||
return {
|
||||
collect(value, callback) {
|
||||
clearTimeout(state.id);
|
||||
state.items.push(value);
|
||||
state.id = setTimeout(() => {
|
||||
callback(min(), max());
|
||||
state.items = [];
|
||||
state.id = null;
|
||||
}, delay);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const unpatchControlPanel = patch(ControlPanel.prototype, {
|
||||
scrollValueCollector: undefined,
|
||||
/** @type {Number}*/
|
||||
scrollHeaderGap: undefined,
|
||||
setup() {
|
||||
this._super();
|
||||
this.state = useState({
|
||||
mobileSearchMode: this.props.withBreadcrumbs ? "" : "quick",
|
||||
});
|
||||
this.ui = deviceContext;
|
||||
super.setup();
|
||||
this.scrollValueCollector = minMaxCollector(100);
|
||||
this.scrollHeaderGap = 2;
|
||||
},
|
||||
setMobileSearchMode(ev) {
|
||||
this.state.mobileSearchMode = ev.detail;
|
||||
onScrollThrottled() {
|
||||
if (this.isScrolling) {
|
||||
return;
|
||||
}
|
||||
this.isScrolling = true;
|
||||
browser.requestAnimationFrame(() => (this.isScrolling = false));
|
||||
|
||||
/** @type {HTMLElement}*/
|
||||
const rootEl = this.root.el;
|
||||
const scrollTop = this.getScrollingElement().scrollTop;
|
||||
const activeAnimation = scrollTop > this.initialScrollTop;
|
||||
|
||||
rootEl.classList.toggle(STICKY_CLASS, activeAnimation);
|
||||
this.scrollValueCollector.collect(scrollTop - this.oldScrollTop, (min, max) => {
|
||||
const delta = min + max;
|
||||
if (delta < -this.scrollHeaderGap || delta > this.scrollHeaderGap) {
|
||||
rootEl.style.top = `${delta < 0 ? -rootEl.clientHeight : 0}px`;
|
||||
}
|
||||
});
|
||||
|
||||
this.oldScrollTop = scrollTop;
|
||||
},
|
||||
});
|
||||
|
||||
// Patch control panel to add states for mobile quick search
|
||||
patch(ControlPanel.prototype, "web_responsive.ControlPanelMobile", {
|
||||
setup() {
|
||||
this._super();
|
||||
this.state = useState({
|
||||
mobileSearchMode: "",
|
||||
});
|
||||
this.ui = deviceContext;
|
||||
},
|
||||
setMobileSearchMode(ev) {
|
||||
this.state.mobileSearchMode = ev.detail;
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(LegacyControlPanel.components, {Dropdown});
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
/* Copyright 2018 Tecnativa - Jairo Llopis
|
||||
* Copyright 2021 ITerra - Sergey Shebanin
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
|
||||
|
||||
// Make enough space for search panel filters buttons
|
||||
.o_control_panel {
|
||||
// There is no media breakpoint for XL upper bound
|
||||
@include media-breakpoint-up(lg) {
|
||||
@media (max-width: 1360px) {
|
||||
.o_cp_top_left,
|
||||
.o_cp_bottom_left {
|
||||
width: 40%;
|
||||
}
|
||||
.o_cp_top_right,
|
||||
.o_cp_bottom_right {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
}
|
||||
// For FULL HD devices
|
||||
@media (min-width: 1900px) {
|
||||
.o_cp_top_left,
|
||||
.o_cp_bottom_left {
|
||||
width: 60%;
|
||||
}
|
||||
.o_cp_top_right,
|
||||
.o_cp_bottom_right {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
@include media-breakpoint-only(md) {
|
||||
.o_search_options_hide_labels .o_dropdown_title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.o_cp_bottom_right {
|
||||
height: 10%;
|
||||
}
|
||||
// Mobile Control panel (breadcrumbs, search box, buttons...)
|
||||
@include media-breakpoint-down(sm) {
|
||||
// Avoid horizontal scrolling of control panel.
|
||||
// It doesn't work on iOS Safari, but it looks similar as
|
||||
// without this patch. With this patch it looks better for
|
||||
// other browsers.
|
||||
|
||||
// Arrange buttons to use space better
|
||||
.o_cp_top_left,
|
||||
.o_cp_top_right {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.o_cp_top_left {
|
||||
flex-basis: 89%;
|
||||
max-width: 89%;
|
||||
}
|
||||
|
||||
.o_cp_top_right {
|
||||
flex-basis: 11%;
|
||||
}
|
||||
|
||||
.o_cp_bottom {
|
||||
position: relative; // Necessary for dropdown menu positioning
|
||||
display: block;
|
||||
margin: 0;
|
||||
min-height: 30px !important;
|
||||
}
|
||||
|
||||
.o_cp_bottom_left {
|
||||
float: left;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.o_cp_bottom_right {
|
||||
float: right;
|
||||
padding-left: 10px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.o_cp_bottom_right,
|
||||
.o_cp_pager {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.o_cp_pager {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.o_list_selection_box {
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.o_cp_action_menus {
|
||||
padding-right: 0;
|
||||
.o_dropdown_title,
|
||||
.fa-chevron-right,
|
||||
.fa-chevron-down {
|
||||
display: none;
|
||||
}
|
||||
.dropdown-toggle {
|
||||
margin: 0px 2px;
|
||||
height: 100%;
|
||||
}
|
||||
.dropdown {
|
||||
height: 100%;
|
||||
}
|
||||
@include media-breakpoint-down(xs) {
|
||||
.dropdown {
|
||||
position: static;
|
||||
}
|
||||
.dropdown-menu {
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 35px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide all but 2 last breadcrumbs, and render 2nd-to-last as arrow
|
||||
.breadcrumb-item {
|
||||
&:not(.active):not(.o_back_button) {
|
||||
padding-left: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&.o_back_button {
|
||||
&::before {
|
||||
color: var(--primary);
|
||||
content: "\f060"; // .fa-arrow-left
|
||||
cursor: pointer;
|
||||
font-family: FontAwesome;
|
||||
}
|
||||
|
||||
a {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ellipsize long breadcrumbs
|
||||
.breadcrumb {
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// In case you install `mail`, there is a mess on BS vs inline styles
|
||||
// we need to fix
|
||||
.o_cp_buttons .btn.d-block:not(.d-none) {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.o_searchview_input_container > .o_searchview_autocomplete {
|
||||
left: 0;
|
||||
right: 0;
|
||||
> li {
|
||||
padding: 10px 0px;
|
||||
}
|
||||
}
|
||||
.o_searchview_quick {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
.o_searchview_input_container {
|
||||
flex: 1 1 auto;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
.o_searchview {
|
||||
padding: 1px 0px 3px 0px;
|
||||
&.o_searchview_mobile {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Filter Menu
|
||||
// Cut long filters names in the filters menu
|
||||
.o_filter_menu {
|
||||
.o_menu_item {
|
||||
@include media-breakpoint-up(md) {
|
||||
max-width: 250px;
|
||||
}
|
||||
a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Enable scroll on dropdowns
|
||||
.o_cp_buttons .dropdown-menu {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
// Dropdown with buttons to switch the view type
|
||||
.o_cp_switch_buttons.dropdown-menu {
|
||||
align-content: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
padding: 0;
|
||||
|
||||
.btn {
|
||||
border: {
|
||||
bottom: 0;
|
||||
radius: 0;
|
||||
top: 0;
|
||||
}
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Mobile search bar full screen mode
|
||||
.o_cp_mobile_search {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
z-index: $zindex-modal;
|
||||
overflow: auto;
|
||||
.o_mobile_search_header {
|
||||
background-color: var(--mobileSearch__header-bg, #{$o-brand-odoo});
|
||||
display: flex;
|
||||
min-height: $o-navbar-height;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.o_mobile_search_button {
|
||||
color: white;
|
||||
|
||||
&:active {
|
||||
background-color: darken($o-brand-primary, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_searchview_input_container {
|
||||
display: flex;
|
||||
padding: 15px 20px 0 20px;
|
||||
position: relative;
|
||||
.o_searchview_input {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid $o-brand-primary;
|
||||
}
|
||||
.o_searchview_facet {
|
||||
display: inline-flex;
|
||||
order: 1;
|
||||
}
|
||||
.o_searchview_autocomplete {
|
||||
top: 3rem;
|
||||
}
|
||||
}
|
||||
.o_mobile_search_filter {
|
||||
padding-bottom: 15%;
|
||||
> .dropdown {
|
||||
flex-direction: column;
|
||||
line-height: 2rem;
|
||||
width: 100%;
|
||||
margin: 15px 5px 0px 5px;
|
||||
border: solid 1px darken($gray-200, 20%);
|
||||
}
|
||||
.dropdown.show > .dropdown-toggle {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
.dropdown-toggle {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
&:after {
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
.dropdown-item:before {
|
||||
top: auto;
|
||||
}
|
||||
.dropdown-item.focus {
|
||||
background-color: white;
|
||||
}
|
||||
.dropdown-menu {
|
||||
// Here we use !important because of popper js adding custom style
|
||||
// to element so to override it use !important
|
||||
position: relative !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
color: $gray-600;
|
||||
.divider {
|
||||
margin: 0px;
|
||||
}
|
||||
> li > a {
|
||||
padding: 10px 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_mobile_search_show_result {
|
||||
padding: 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2021 Sergey Shebanin
|
||||
Copyright 2023 Onestein - Anjeel Haria
|
||||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
|
||||
<templates>
|
||||
<!-- Legacy control panel templates -->
|
||||
<t t-inherit="web.Legacy.ControlPanel" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//nav[hasclass('o_cp_switch_buttons')]" position="replace">
|
||||
<t t-if="props.views.length gt 1">
|
||||
<t t-if="ui.size lt= ui.SIZES.LG">
|
||||
<Dropdown
|
||||
position="'bottom-end'"
|
||||
menuClass="'d-inline-flex o_cp_switch_buttons'"
|
||||
togglerClass="'btn btn-link'"
|
||||
>
|
||||
<t t-set-slot="toggler">
|
||||
<i
|
||||
class="fa fa-lg o_switch_view"
|
||||
t-attf-class="o_{{env.view.type}} {{env.view.icon}} {{ props.views.filter(view => view.type === env.view.type)[0].icon }} {{env.view.active ? 'active' : ''}}"
|
||||
/>
|
||||
</t>
|
||||
<t t-foreach="props.views" t-as="view" t-key="view.type">
|
||||
<t t-call="web.ViewSwitcherButton" />
|
||||
</t>
|
||||
</Dropdown>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<nav
|
||||
class="btn-group o_cp_switch_buttons"
|
||||
role="toolbar"
|
||||
aria-label="View switcher"
|
||||
>
|
||||
<t t-foreach="props.views" t-as="view" t-key="view.type">
|
||||
<t t-call="web.ViewSwitcherButton" />
|
||||
</t>
|
||||
</nav>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_searchview')]" position="replace">
|
||||
<div
|
||||
t-if="props.withSearchBar"
|
||||
class="o_searchview"
|
||||
t-att-class="state.mobileSearchMode == 'quick' ? 'o_searchview_quick' : 'o_searchview_mobile'"
|
||||
role="search"
|
||||
aria-autocomplete="list"
|
||||
t-on-click.self="() => { state.mobileSearchMode = ui.isSmall ? 'quick' : '' }"
|
||||
>
|
||||
<t t-if="!ui.isSmall">
|
||||
<i
|
||||
class="o_searchview_icon fa fa-search"
|
||||
title="Search..."
|
||||
role="img"
|
||||
aria-label="Search..."
|
||||
/>
|
||||
<SearchBar fields="fields" />
|
||||
</t>
|
||||
<t t-if="ui.isSmall">
|
||||
<t t-if="state.mobileSearchMode == 'quick'">
|
||||
<button
|
||||
t-if="props.withBreadcrumbs"
|
||||
class="btn btn-link fa fa-arrow-left"
|
||||
t-on-click.stop="() => { state.mobileSearchMode = '' }"
|
||||
/>
|
||||
<SearchBar fields="fields" />
|
||||
<button
|
||||
class="btn fa fa-filter"
|
||||
t-on-click.stop="() => { state.mobileSearchMode = 'full' }"
|
||||
/>
|
||||
</t>
|
||||
<t
|
||||
t-if="state.mobileSearchMode == 'full'"
|
||||
t-call="web_responsive.LegacyMobileSearchView"
|
||||
/>
|
||||
<t t-if="state.mobileSearchMode == ''">
|
||||
<button
|
||||
class="btn btn-link fa fa-search"
|
||||
t-on-click.stop="() => { state.mobileSearchMode = 'quick' }"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_cp_top_left')]" position="attributes">
|
||||
<attribute
|
||||
name="t-att-class"
|
||||
t-translation="off"
|
||||
>ui.isSmall and state.mobileSearchMode == 'quick' ? 'o_hidden' : ''</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_search_options')]" position="attributes">
|
||||
<attribute name="t-if" t-translation="off">!ui.isSmall</attribute>
|
||||
<attribute
|
||||
name="t-att-class"
|
||||
t-translation="off"
|
||||
>ui.size == ui.SIZES.MD ? 'o_search_options_hide_labels' : ''</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
<t t-name="web_responsive.LegacyMobileSearchView" owl="1">
|
||||
<div class="o_cp_mobile_search">
|
||||
<div class="o_mobile_search_header">
|
||||
<button
|
||||
type="button"
|
||||
class="o_mobile_search_button btn"
|
||||
t-on-click="() => state.mobileSearchMode = false"
|
||||
>
|
||||
<i class="fa fa-arrow-left" />
|
||||
<strong class="ms-2">FILTER</strong>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="o_mobile_search_button btn"
|
||||
t-on-click="() => this.model.dispatch('clearQuery')"
|
||||
>
|
||||
CLEAR
|
||||
</button>
|
||||
</div>
|
||||
<SearchBar fields="fields" />
|
||||
<div class="o_mobile_search_filter o_search_options mb8 mt8 ml16 mr16">
|
||||
<FilterMenu
|
||||
t-if="props.searchMenuTypes.includes('filter')"
|
||||
class="o_filter_menu"
|
||||
fields="fields"
|
||||
/>
|
||||
<GroupByMenu
|
||||
t-if="props.searchMenuTypes.includes('groupBy')"
|
||||
class="o_group_by_menu"
|
||||
fields="fields"
|
||||
/>
|
||||
<ComparisonMenu
|
||||
t-if="props.searchMenuTypes.includes('comparison') and model.get('filters', f => f.type === 'comparison').length"
|
||||
class="o_comparison_menu"
|
||||
/>
|
||||
<FavoriteMenu
|
||||
t-if="props.searchMenuTypes.includes('favorite')"
|
||||
class="o_favorite_menu"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-primary o_mobile_search_show_result fixed-bottom"
|
||||
t-on-click="() => { state.mobileSearchMode = (props.withBreadcrumbs ? '' : 'quick') }"
|
||||
>
|
||||
<t>SEE RESULT</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web_responsive.SearchBar" owl="1">
|
||||
<div>
|
||||
<t t-if="!env.isSmall" t-call="web.SearchBar" />
|
||||
<t t-if="env.isSmall">
|
||||
<t t-if="props.mobileSearchMode == 'quick'">
|
||||
<div class="o_searchview o_searchview_quick">
|
||||
<button
|
||||
t-if="props.withBreadcrumbs"
|
||||
class="btn btn-link fa fa-arrow-left"
|
||||
t-on-click.stop="() => this.trigger('set-mobile-view', '')"
|
||||
/>
|
||||
<div class="o_searchview_input_container">
|
||||
<t t-call="web.SearchBar.Facets" />
|
||||
<t t-call="web.SearchBar.Input" />
|
||||
<t t-if="items.length">
|
||||
<t t-call="web.SearchBar.Items" />
|
||||
</t>
|
||||
</div>
|
||||
<button
|
||||
class="btn fa fa-filter"
|
||||
t-on-click.stop="() => this.trigger('set-mobile-view', 'full')"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
<t
|
||||
t-if="props.mobileSearchMode == 'full'"
|
||||
t-call="web_responsive.MobileSearchView"
|
||||
/>
|
||||
<t t-if="props.mobileSearchMode == ''">
|
||||
<div
|
||||
class="o_searchview o_searchview_mobile"
|
||||
role="search"
|
||||
aria-autocomplete="list"
|
||||
t-on-click.stop="() => this.trigger('set-mobile-view', 'quick')"
|
||||
>
|
||||
<button class="btn btn-link fa fa-search" />
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="web_responsive.MobileSearchView" owl="1">
|
||||
<div class="o_searchview">
|
||||
<div class="o_cp_mobile_search">
|
||||
<div class="o_mobile_search_header">
|
||||
<span
|
||||
class="o_mobile_search_close float-left mt16 mb16 mr8 ml16"
|
||||
t-on-click.stop="() => this.trigger('set-mobile-view', 'quick')"
|
||||
>
|
||||
<i class="fa fa-arrow-left" />
|
||||
<strong class="float-right ml8">FILTER</strong>
|
||||
</span>
|
||||
<span
|
||||
class="float-right o_mobile_search_clear_facets mt16 mr16"
|
||||
t-on-click.stop="() => env.searchModel.clearQuery()"
|
||||
>
|
||||
<t>CLEAR</t>
|
||||
</span>
|
||||
</div>
|
||||
<div class="o_searchview_input_container">
|
||||
<t t-call="web.SearchBar.Facets" />
|
||||
<t t-call="web.SearchBar.Input" />
|
||||
<t t-if="items.length">
|
||||
<t t-call="web.SearchBar.Items" />
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_mobile_search_filter o_search_options mb8 mt8 ml16 mr16">
|
||||
<t t-foreach="props.searchMenus" t-as="menu" t-key="menu.key">
|
||||
<t t-component="menu.Component" />
|
||||
</t>
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-primary o_mobile_search_show_result fixed-bottom"
|
||||
t-on-click.stop="() => this.trigger('set-mobile-view', '')"
|
||||
>
|
||||
<t>SEE RESULT</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
Reference in New Issue
Block a user