Merge branch 'feature/SUPPORT-8689_fix_esnsi_okopf' into develop

# Conflicts:
#	backend/pom.xml
#	distribution/pom.xml
#	frontend/pom.xml
#	pom.xml
#	resources/pom.xml
This commit is contained in:
Artyom Hackimullin 2024-11-14 16:13:06 +03:00
commit e4a890b7b4
18 changed files with 349 additions and 101 deletions

View file

@ -5,7 +5,7 @@
<parent>
<groupId>ru.micord.ervu.lkrp</groupId>
<artifactId>ul</artifactId>
<version>1.10.0-SNAPSHOT</version>
<version>1.9.2-SNAPSHOT</version>
</parent>
<groupId>ru.micord.ervu.lkrp.ul</groupId>
<artifactId>backend</artifactId>

View file

@ -1,14 +1,17 @@
package ervu.client.okopf;
import java.io.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.TimeoutException;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.zip.ZipInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
@ -19,37 +22,26 @@ import org.springframework.stereotype.Component;
*/
@Component
public class EsnsiOkopfClient {
private static final Logger logger = LoggerFactory.getLogger(EsnsiOkopfClient.class);
@Value("${esnsi.okopf.url}")
private String uri;
private String url;
@Retryable(value = {TimeoutException.class}, backoff =
@Backoff(delay = 2000))
@Retryable(maxAttemptsExpression = "${esnsi.okopf.retry.max.attempts.load:3}", backoff =
@Backoff(delayExpression = "${esnsi.okop.retry.delay.load:1000}"))
public String getJsonOkopFormData() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.GET()
.build();
try {
HttpResponse<InputStream> response = client.send(request,
HttpResponse.BodyHandlers.ofInputStream()
);
if (response.statusCode() >= 200 && response.statusCode() <= 202) {
return unzipFile(new ZipInputStream(response.body()));
try (BufferedInputStream in = new BufferedInputStream(new URL(url).openStream());
ZipInputStream archiveStream = new ZipInputStream(in);
BufferedReader br = new BufferedReader(
new InputStreamReader(archiveStream, StandardCharsets.UTF_8))) {
if (Objects.nonNull(archiveStream.getNextEntry())) {
return br.lines().collect(Collectors.joining(System.lineSeparator()));
}
throw new RuntimeException("The returned status " + response.statusCode() + " is incorrect. Json file has not be unzip");
logger.warn("Esnsi okopf client received an empty archive in response.");
}
catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
catch (IOException e) {
logger.error("Failed to send HTTP request or process the response for okopf file.", e);
}
}
private String unzipFile(ZipInputStream zis) throws IOException {
if (zis.getNextEntry() != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(zis.readAllBytes())));
return br.lines().collect(Collectors.joining(System.lineSeparator()));
}
throw new RuntimeException("ZipInputStream is empty and has not been unzipped");
return null;
}
}

View file

@ -24,6 +24,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import static org.springframework.scheduling.config.ScheduledTaskRegistrar.CRON_DISABLED;
import static org.springframework.util.StringUtils.hasText;
/**
@ -60,16 +61,21 @@ public class EsnsiOkopfSchedulerServiceImpl implements EsnsiOkopfSchedulerServic
@SchedulerLock(name = "loadOkopf")
@Transactional
public void load() {
logger.info("Loading okopf file");
String data = esnsiOkopfClient.getJsonOkopFormData();
try {
logger.info("Loading okopf file");
String data = esnsiOkopfClient.getJsonOkopFormData();
OkopfOrgModel orgModel = mapper.readValue(data, OkopfOrgModel.class);
int currentVersion = mapper.readTree(data).findValue("version").asInt();
List<OkopfModel> okopfRecords = mapToOkopfRecords(orgModel.getData(), currentVersion);
okopfDao.save(okopfRecords);
if (hasText(data)) {
OkopfOrgModel orgModel = mapper.readValue(data, OkopfOrgModel.class);
int currentVersion = mapper.readTree(data).findValue("version").asInt();
List<OkopfModel> okopfRecords = mapToOkopfRecords(orgModel.getData(), currentVersion);
okopfDao.save(okopfRecords);
}
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
throw new RuntimeException(
"Failed to process the JSON response from the esnsi okopf client. Invalid or malformed JSON.",
e
);
}
}

View file

@ -0,0 +1,11 @@
package ru.micord.ervu.property.grid;
/**
* @author gulnaz
*/
public enum FilterType {
TEXT,
DATE,
NUMBER,
SET
}

View file

@ -7,4 +7,5 @@ public class StaticColumn {
public String column;
public String type;
public FilterType filterType;
}

View file

@ -779,7 +779,12 @@ JBPM использует 3 корневых категории логирова
#### Взаимодействие с ЕСНСИ в части получения справочника ОКОПФ
- `ESNSI_OKOPF_URL` - url который обращается к еснси для получения справочника и скачивает данные спровочников организации в виде заархивированного json файла.
- `ESNSI_OKOPF_CRON_LOAD` - настройка, которая указывет расписание для загрузки справочника окопф и сохранение данных по справкам в БД
- `ESNSI_OKOPF_CRON_LOAD` - настройка, которая указывет расписание для загрузки справочника окопф и
сохранение данных по справкам в БД
- `ESNSI_OKOPF_RETRY_DELAY_LOAD` - настройка, которая указывет на повторную попытку загрузить
справочник окопф с задержкой. По умолчанию задержка по времени 1000 ms
- `ESNSI_OKOPF_RETRY_MAX_ATTEMPTS_LOAD` - настройка, которая указывет на максимальное кол-во попыток
повторно загрузить справочник окопф. По умолчанию 3 попытки
#### Взаимодействие с WebDav

View file

@ -36,6 +36,8 @@ ERVU_KAFKA_JOURNAL_REQUEST_TOPIC=ervu.organization.journal.request
ERVU_KAFKA_JOURNAL_REPLY_TOPIC=ervu.organization.journal.response
ESNSI_OKOPF_URL=https://esnsi.gosuslugi.ru/rest/ext/v1/classifiers/11465/file?extension=JSON&encoding=UTF_8
ESNSI_OKOPF_CRON_LOAD=0 0 */1 * * *
ESNSI_OKOPF_RETRY_MAX_ATTEMPTS_LOAD=3
ESNSI_OKOPF_RETRY_DELAY_LOAD=1000
ERVU_KAFKA_SECURITY_PROTOCOL=SASL_PLAINTEXT
ERVU_KAFKA_SASL_MECHANISM=SCRAM-SHA-256
ERVU_KAFKA_USERNAME=user1

View file

@ -84,6 +84,8 @@
<property name="ervu.kafka.password" value="Blfi9d2OFG"/>
<property name="esnsi.okopf.cron.load" value="0 0 */1 * * *"/>
<property name="esnsi.okopf.url" value="https://esnsi.gosuslugi.ru/rest/ext/v1/classifiers/11465/file?extension=JSON&amp;encoding=UTF_8"/>
<property name="esnsi.okop.retry.delay.load" value="1000"/>
<property name="esnsi.okopf.retry.max.attempts.load" value="3"/>
<property name="ervu.kafka.journal.request.topic" value="ervu.organization.journal.request"/>
<property name="ervu.kafka.journal.reply.topic" value="ervu.organization.journal.response"/>
<property name="db.journal.excluded.statuses" value="Направлено в ЕРВУ,Получен ЕРВУ"/>

View file

@ -4,7 +4,7 @@
<parent>
<groupId>ru.micord.ervu.lkrp</groupId>
<artifactId>ul</artifactId>
<version>1.10.0-SNAPSHOT</version>
<version>1.9.2-SNAPSHOT</version>
</parent>
<groupId>ru.micord.ervu.lkrp.ul</groupId>

View file

@ -4,7 +4,7 @@
<parent>
<groupId>ru.micord.ervu.lkrp</groupId>
<artifactId>ul</artifactId>
<version>1.10.0-SNAPSHOT</version>
<version>1.9.2-SNAPSHOT</version>
</parent>
<groupId>ru.micord.ervu.lkrp.ul</groupId>

View file

@ -202,7 +202,7 @@
.webbpm.ervu_lkrp_ul .warning-group + field-set {
margin-top: var(--indent-medium);
}
.webbpm.ervu_lkrp_ul .warning-group + .warning-group,
.webbpm.ervu_lkrp_ul .warning-group + .warning-group,
.webbpm.ervu_lkrp_ul .warning-group + .data-group {
margin-top: var(--indent-small);
}
@ -300,20 +300,20 @@
margin-bottom: 16px;
}
.webbpm.ervu_lkrp_ul .paragraph-group > .vertical-container > *:last-child {
margin-bottom: 0;
margin-bottom: 0;
}
.webbpm.ervu_lkrp_ul .paragraph-group + .paragraph-group {
padding-top: 24px;
margin-top: 24px;
border-top: 1px solid var(--border-light);
}
}
.webbpm.ervu_lkrp_ul .fieldset {
padding: 24px;
margin-bottom: 0;
border: 1px solid var(--border-light);
border-radius: 4px;
background-color: var(--bg-light);
background-color: var(--bg-light);
box-shadow: none;
}
.webbpm.ervu_lkrp_ul .fieldset legend + div {
@ -392,7 +392,7 @@
margin-top: var(--indent-medium);
}
.webbpm.ervu_lkrp_ul input,
.webbpm.ervu_lkrp_ul input,
.webbpm.ervu_lkrp_ul button {
border-radius: 4px;
box-shadow: none !important;
@ -426,7 +426,7 @@
.webbpm.ervu_lkrp_ul ag-grid-angular {
font-family: 'Inter';
}
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-popup .ag-select-list-item {
font-size: var(--size-text-secondary);
@ -440,31 +440,39 @@
width: 24px;
height: 24px;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-icon:is(.ag-icon-small-down, .ag-icon-filter)::before {
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-icon:is(.ag-icon-small-down)::before {
content: "";
width: 24px;
height: 24px;
top: 0;
left: 0;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-icon.ag-icon-small-down::before {
background-image: url(../img/svg/arrow-left.svg);
transform: rotate(-90deg);
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-icon.ag-icon-filter::before {
background-image: url(../img/svg/filter.svg);
top: -4px;
left: -4px;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-icon-menu {
background: transparent url(../img/svg/filter.svg) center no-repeat;
color: transparent;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-header-cell-menu-button:not(.ag-header-menu-always-show) {
opacity: unset;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-filter-select {
font-size: var(--size-text-secondary);
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-filter-select .ag-picker-field-wrapper {
font-family: 'InterSB';
border: 0;
box-shadow: none !important;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-filter-select .ag-picker-field-wrapper .ag-picker-field-display {
margin: 0;
}
@ -472,6 +480,7 @@
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-filter-body {
margin-bottom: 0;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-filter-body input {
color: var(--color-light);
font-size: var(--size-text-secondary);
@ -479,6 +488,43 @@
padding: 6px 12px !important;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-set-filter {
min-width: 100px;
padding: 10px 12px;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-set-filter-item + .ag-set-filter-item {
margin-top: 8px;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-filter .ag-filter-checkbox {
width: 24px;
height: 24px;
border: 2px solid var(--color-link);
border-radius: 4px;
position: relative;
margin-right: 12px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
vertical-align: text-bottom;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-filter .ag-filter-checkbox:before {
content: '';
color: white;
position: absolute;
top: 3px;
left: 3px;
width: 14px;
height: 14px;
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-filter .ag-filter-checkbox:checked:before {
border-radius: 2px;
background: var(--color-link) url(../img/svg/input-checked.svg);
}
.webbpm.ervu_lkrp_ul ag-grid-angular .ag-header-row {
font-family: 'InterSB';
}
@ -528,7 +574,7 @@
flex-direction: column-reverse;
}
.webbpm.ervu_lkrp_ul .main-block .left-block {
padding-right: 0;
padding-right: 0;
margin-top: var(--indent-medium);
}
@ -542,8 +588,8 @@
top: 20px;
left: auto;
right: 20px;
}
}
.webbpm.ervu_lkrp_ul .left-block {
width: 100%;
}
@ -702,7 +748,7 @@
background: none;
}
.webbpm.ervu_lkrp_ul .modal.show .modal-content .warning-group > div > * + *:not([hidden]) {
margin-top: 8px;
margin-top: 8px;
}
.webbpm.ervu_lkrp_ul .modal.show .modal-content hyper-link.btn {
padding: 0;
@ -711,7 +757,7 @@
}
.webbpm.ervu_lkrp_ul .modal.show .modal-content hyper-link.btn .hyper-link {
padding: 13px 38px;
border: 1px solid var(--color-link);
border: 1px solid var(--color-link);
border-radius: 4px;
}
.webbpm.ervu_lkrp_ul .modal.show .modal-content hyper-link.btn .hyper-link:hover {
@ -814,7 +860,7 @@
.webbpm.ervu_lkrp_ul .modal.show ervu-file-upload .selected-file .selected-file-size::before {
position: relative;
content: "|";
margin-right: 8px;
margin-right: 8px;
}
.webbpm.ervu_lkrp_ul .modal.show ervu-file-upload .selected-file .selected-file-delete-btn {
color: var(--color-link);
@ -871,7 +917,7 @@
/* temp fix + add flex-wrap*/
.webbpm.ervu_lkrp_ul :is(.fieldset, .warning-group) .horizontal-container text + button-component:not(.info) {
margin-top: -2px;
margin-top: -2px;
}
.webbpm.ervu_lkrp_ul .dialog-link {

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.56192 6.65976L5.83968 9.93751L11.4379 4.33928" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 179 B

View file

@ -1,12 +1,15 @@
import {ColDef, ICellRendererFunc, SuppressKeyboardEventParams} from "ag-grid-community";
import {
ColDef,
DateFilter,
ICellRendererFunc,
SuppressKeyboardEventParams
} from "ag-grid-community";
import {
DateTimeUtil,
DefaultTooltip,
GridCellTooltipUtils,
GridColumnComparatorUtils,
GridColumnFilterUtils,
GridColumnKeyboardUtils,
GridSettingHeader,
GridValueFormatterUtils,
GridValueRendererUtils,
PinnedType
@ -14,6 +17,7 @@ import {
import {Moment} from "moment";
import * as moment from "moment-timezone";
import {StaticGridColumn} from "../../../generated/ru/micord/ervu/property/grid/StaticGridColumn";
import {CustomGridColumnFilterUtils} from "./filter/CustomGridColumnFilterUtils";
export class StaticColumnInitializer {
@ -50,40 +54,6 @@ export class StaticColumnInitializer {
let type = column.field.type;
if (type != null) {
if (column.filter !== false) {
columnDef.floatingFilter = gridRef.floatingFilter;
columnDef.filter = GridColumnFilterUtils.columnFilter(type);
if (columnDef.filter === 'agDateColumnFilter') {
columnDef.filterParams = {
comparator: function (filterLocalDateAtMidnight, cellValue) {
if (!cellValue) {
return -1;
}
let filterMoment: Moment = moment.utc(filterLocalDateAtMidnight)
.add(-filterLocalDateAtMidnight.getTimezoneOffset(), 'm');
let cellMoment: Moment = DateTimeUtil.parseToMidnightUTC(cellValue);
if (filterMoment.isSame(cellMoment)) {
return 0;
}
if (cellMoment.isBefore(filterMoment)) {
return -1;
}
if (cellMoment.isAfter(filterMoment)) {
return 1;
}
},
browserDatePicker: true,
};
}
}
if (gridRef.getRowModelType() == "clientSide") {
columnDef.comparator = GridColumnComparatorUtils.columnComparator(type);
}
@ -91,6 +61,39 @@ export class StaticColumnInitializer {
columnDef.cellRenderer = gridRef.createRenderer(column);
}
if (column.filter !== false) {
columnDef.floatingFilter = gridRef.floatingFilter;
columnDef.filter = CustomGridColumnFilterUtils.columnFilter(column.field.filterType);
if (columnDef.filter === DateFilter) {
columnDef.filterParams = {
comparator: function (filterLocalDateAtMidnight, cellValue) {
if (!cellValue) {
return -1;
}
let filterMoment: Moment = moment.utc(filterLocalDateAtMidnight)
.add(-filterLocalDateAtMidnight.getTimezoneOffset(), 'm');
let cellMoment: Moment = DateTimeUtil.parseToMidnightUTC(cellValue);
if (filterMoment.isSame(cellMoment)) {
return 0;
}
if (cellMoment.isBefore(filterMoment)) {
return -1;
}
if (cellMoment.isAfter(filterMoment)) {
return 1;
}
},
browserDatePicker: true,
};
}
}
columnDef.suppressKeyboardEvent = (params: SuppressKeyboardEventParams) => {
return GridColumnKeyboardUtils.suppressHomeAndEndKeyboardEvent(params);
}

View file

@ -0,0 +1,24 @@
import {FilterType} from "../../../../generated/ru/micord/ervu/property/grid/FilterType";
import {DateFilter, NumberFilter, TextFilter} from "ag-grid-community";
import {SetFilter} from "./SetFilter";
export class CustomGridColumnFilterUtils {
public static columnFilter(type: FilterType) {
if (!type) {
return null;
}
switch (type) {
case FilterType.NUMBER:
return NumberFilter;
case FilterType.DATE:
return DateFilter;
case FilterType.SET:
return SetFilter;
case FilterType.TEXT:
default:
return TextFilter;
}
}
}

View file

@ -0,0 +1,111 @@
import {AgPromise, IDoesFilterPassParams, IFilterComp, IFilterParams} from "ag-grid-community";
export class SetFilter implements IFilterComp {
private OPTION_TEMPLATE = `<label class="ag-set-filter-item">
<input type="checkbox" class="ag-filter-checkbox" checked/>
<span class="ag-filter-value"></span>
</label>`;
private eGui!: HTMLDivElement;
private selectAll: HTMLInputElement;
private checkboxes: HTMLInputElement[] = [];
private values: any[] = [];
private initialValues: any[];
private filterActive: boolean;
private filterChangedCallback!: (additionalEventAttributes?: any) => void;
private filterParams!: IFilterParams;
private valueType: string;
init(params: IFilterParams): void {
this.eGui = document.createElement('div');
this.eGui.className = 'ag-set-filter';
let index = 0;
this.selectAll = this.initCheckBox('selectAll', 'Все', index);
this.checkboxes.push(this.selectAll);
params.api.getRenderedNodes()
.map(node => node.data[params.colDef.field])
.sort((n1, n2) => n1 > n2 ? 1 : n1 < n2 ? -1 : 0)
.forEach(value => {
if (this.values.includes(value)) {
return;
}
index++;
let id = `option-${index}`;
let checkbox = this.initCheckBox(id, value, index);
this.checkboxes.push(checkbox);
this.values.push(value);
});
this.initialValues = this.values.slice();
this.filterParams = params;
this.filterActive = false;
this.filterChangedCallback = params.filterChangedCallback;
if (this.values.length > 0) {
this.valueType = typeof this.values[0];
}
};
private initCheckBox(id: string, value: string, index: number): HTMLInputElement {
this.eGui.insertAdjacentHTML('beforeend', this.OPTION_TEMPLATE);
this.eGui.querySelectorAll('.ag-filter-value')[index].innerHTML = value;
let checkbox = this.eGui.querySelectorAll('.ag-filter-checkbox')[index] as HTMLInputElement;
checkbox.setAttribute('id', id);
checkbox.addEventListener('change', this.onCheckBoxChanged.bind(this));
return checkbox;
}
getGui(): HTMLDivElement {
return this.eGui;
};
onCheckBoxChanged(event: any) {
let checked = event.target.checked;
if (event.target === this.selectAll) {
this.checkboxes.forEach(checkbox => checkbox.checked = checked);
this.values = checked ? this.initialValues.slice() : [];
}
else {
let value = event.target.nextElementSibling.textContent;
value = this.valueType === 'number' ? +value : value;
if (checked) {
this.values.push(value);
if (this.values.length == this.initialValues.length) {
this.selectAll.checked = true;
}
}
else {
let index = this.values.indexOf(value);
this.values.splice(index, 1);
this.selectAll.checked = false;
}
}
this.filterActive = !this.selectAll.checked;
this.filterChangedCallback();
}
doesFilterPass(params: IDoesFilterPassParams): boolean {
let { field } = this.filterParams.colDef;
return this.values.includes(params.data[field]);
}
getModel(): any {
return this.isFilterActive() ? { value: this.values } : null;
}
isFilterActive(): boolean {
return this.filterActive;
}
setModel(model: any): void | AgPromise<void> {
this.values = model == null ? [] : model.value;
}
destroy(): void {
this.checkboxes.forEach(checkBox => checkBox.removeEventListener('change', this.onCheckBoxChanged.bind(this)));
}
}

View file

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ru.micord.ervu.lkrp</groupId>
<artifactId>ul</artifactId>
<version>1.10.0-SNAPSHOT</version>
<version>1.9.2-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>backend</module>

View file

@ -4,7 +4,7 @@
<parent>
<groupId>ru.micord.ervu.lkrp</groupId>
<artifactId>ul</artifactId>
<version>1.10.0-SNAPSHOT</version>
<version>1.9.2-SNAPSHOT</version>
</parent>
<groupId>ru.micord.ervu.lkrp.ul</groupId>

View file

@ -734,6 +734,12 @@
<simple>"departureDateTime"</simple>
</value>
</entry>
<entry>
<key>filterType</key>
<value>
<simple>"DATE"</simple>
</value>
</entry>
<entry>
<key>type</key>
<value>
@ -822,6 +828,12 @@
<simple>"fileName"</simple>
</value>
</entry>
<entry>
<key>filterType</key>
<value>
<simple>"TEXT"</simple>
</value>
</entry>
<entry>
<key>type</key>
<value>
@ -904,6 +916,12 @@
<simple>"filePatternCode"</simple>
</value>
</entry>
<entry>
<key>filterType</key>
<value>
<simple>"SET"</simple>
</value>
</entry>
<entry>
<key>type</key>
<value>
@ -986,6 +1004,12 @@
<simple>"senderFio"</simple>
</value>
</entry>
<entry>
<key>filterType</key>
<value>
<simple>"SET"</simple>
</value>
</entry>
<entry>
<key>type</key>
<value>
@ -1068,6 +1092,12 @@
<simple>"status"</simple>
</value>
</entry>
<entry>
<key>filterType</key>
<value>
<simple>"TEXT"</simple>
</value>
</entry>
<entry>
<key>type</key>
<value>
@ -1150,6 +1180,12 @@
<simple>"filesSentCount"</simple>
</value>
</entry>
<entry>
<key>filterType</key>
<value>
<simple>"NUMBER"</simple>
</value>
</entry>
<entry>
<key>type</key>
<value>
@ -1232,6 +1268,12 @@
<simple>"acceptedFilesCount"</simple>
</value>
</entry>
<entry>
<key>filterType</key>
<value>
<simple>"NUMBER"</simple>
</value>
</entry>
<entry>
<key>type</key>
<value>
@ -1347,8 +1389,8 @@
<container>false</container>
<childrenReordered>false</childrenReordered>
<scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f">
<removed>true</removed>
<expanded>false</expanded>
<removed>true</removed>
</scripts>
<scripts id="38036714-7fff-4404-98d3-b0f5cc846368">
<classRef type="TS">