[IMPROVE] Rewrite Prune Messages as React component (#19900)
parent
9e18df6baa
commit
1ae396fd04
@ -1,18 +1,20 @@ |
||||
|
||||
import { useMemo } from 'react'; |
||||
import { useMemo, lazy } from 'react'; |
||||
|
||||
import { addAction } from '../../../../client/views/room/lib/Toolbox'; |
||||
import { usePermission } from '../../../../client/contexts/AuthorizationContext'; |
||||
|
||||
const template = lazy(() => import('../../../../client/views/room/contextualBar/PruneMessages')); |
||||
|
||||
addAction('clean-history', ({ room }) => { |
||||
const hasPermission = usePermission('clean-channel-history', room._id); |
||||
return useMemo(() => (hasPermission ? { |
||||
groups: ['channel', 'group', 'direct'], |
||||
id: 'clean-history', |
||||
anonymous: true, |
||||
full: true, |
||||
title: 'Prune_Messages', |
||||
icon: 'eraser', |
||||
template: 'cleanHistory', |
||||
template, |
||||
order: 250, |
||||
} : null), [hasPermission]); |
||||
}); |
||||
|
||||
@ -1,158 +0,0 @@ |
||||
<template name="cleanHistory"> |
||||
{{#unless busy}} |
||||
<main class="rc-user-info__scroll"> |
||||
<div class="rc-input"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Newer_than"}}</div> |
||||
<div class="rc-input__wrapper rc-datetime__left"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon block="rc-input__icon-svg" icon="calendar"}} |
||||
</div> |
||||
<input type="date" class="rc-input__element" value="" name="from__date" autocomplete="off" placeholder="YYYY-MM-DD"/> |
||||
</div> |
||||
<div class="rc-input__wrapper rc-datetime__right"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon block="rc-input__icon-svg" icon="clock"}} |
||||
</div> |
||||
<input type="time" class="rc-input__element" value="" name="from__time" autocomplete="off" placeholder="HH:MM"/> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
<div class="rc-input"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Older_than"}}</div> |
||||
<div class="rc-input__wrapper rc-datetime__left"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon block="rc-input__icon-svg" icon="calendar"}} |
||||
</div> |
||||
<input type="date" class="rc-input__element" value="" name="to__date" autocomplete="off" placeholder="YYYY-MM-DD"/> |
||||
</div> |
||||
<div class="rc-input__wrapper rc-datetime__right"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon block="rc-input__icon-svg" icon="clock"}} |
||||
</div> |
||||
<input type="time" class="rc-input__element" value="" name="to__time" autocomplete="off" placeholder="HH:MM"/> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
<div class="rc-input rc-input--usernames"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Only_from_users"}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon block="rc-input__icon-svg" icon="at"}} |
||||
</div> |
||||
<div class="rc-tags"> |
||||
{{#each user in selectedUsers}} |
||||
{{> tag user}} |
||||
{{/each}} |
||||
<input type="text" class="rc-tags__input" placeholder="{{_ "Username_Placeholder"}}" name="users" autocomplete="off"/> |
||||
</div> |
||||
</div> |
||||
{{#with config}} |
||||
{{#if autocomplete 'isShowing'}} |
||||
{{> popupList data=config items=items ready=(autocomplete 'isLoaded')}} |
||||
{{/if}} |
||||
{{/with}} |
||||
</label> |
||||
</div> |
||||
<label class="rc-checkbox"> |
||||
<input type="checkbox" name="inclusive" class="rc-checkbox__input"> |
||||
{{> icon icon="check" block="rc-checkbox__icon"}} |
||||
<span class="rc-checkbox__text rc-text__small">{{_ "Inclusive"}}</span> |
||||
</label> |
||||
<label class="rc-checkbox"> |
||||
<input type="checkbox" name="excludePinned" class="rc-checkbox__input"> |
||||
{{> icon icon="check" block="rc-checkbox__icon"}} |
||||
<span class="rc-checkbox__text rc-text__small">{{_ "RetentionPolicy_DoNotPrunePinned"}}</span> |
||||
</label> |
||||
<label class="rc-checkbox"> |
||||
<input type="checkbox" name="ignoreDiscussion" class="rc-checkbox__input"> |
||||
{{> icon icon="check" block="rc-checkbox__icon"}} |
||||
<span class="rc-checkbox__text rc-text__small">{{_ "RetentionPolicy_DoNotPruneDiscussion"}}</span> |
||||
</label> |
||||
<label class="rc-checkbox"> |
||||
<input type="checkbox" name="ignoreThreads" class="rc-checkbox__input"> |
||||
{{> icon icon="check" block="rc-checkbox__icon"}} |
||||
<span class="rc-checkbox__text rc-text__small">{{_ "RetentionPolicy_DoNotPruneThreads"}}</span> |
||||
</label> |
||||
<label class="rc-checkbox"> |
||||
<input type="checkbox" name="filesOnly" class="rc-checkbox__input"> |
||||
{{> icon icon="check" block="rc-checkbox__icon"}} |
||||
<span class="rc-checkbox__text rc-text__small">{{_ "Files_only"}}</span> |
||||
</label> |
||||
{{#unless validate}} |
||||
<div class="mail-messages__instructions mail-messages__instructions--warning"> |
||||
<div class="mail-messages__instructions-wrapper"> |
||||
{{> icon block="mail-messages__instructions-icon" icon="warning"}} |
||||
<div class="mail-messages__instructions-text"> |
||||
{{warningBox}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{{else}} |
||||
<div class="mail-messages__instructions mail-messages__instructions--warning"> |
||||
<div class="mail-messages__instructions-wrapper"> |
||||
{{> icon block="mail-messages__instructions-icon" icon="modal-error"}} |
||||
<div class="mail-messages__instructions-text"> |
||||
{{validate}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{{/unless}} |
||||
</main> |
||||
<div class="rc-user-info__flex rc-user-info__row"> |
||||
<button class="rc-button rc-button--cancel js-prune" title="{{_ 'Prune'}}" disabled="{{#with validate}}{{.}}{{/with}}">{{_ 'Prune'}}</button> |
||||
</div> |
||||
{{else}} |
||||
{{#unless finished}} |
||||
<main class="rc-user-info__pruning"> |
||||
<p class="pruning__header"> |
||||
{{#unless filesOnly}} |
||||
{{_ "Pruning_messages"}} |
||||
{{else}} |
||||
{{_ "Pruning_files"}} |
||||
{{/unless}} |
||||
</p> |
||||
<div class="pruning-wrapper"> |
||||
{{> icon icon="loading" block="rc-icon"}} |
||||
<div class="pruning__text"> |
||||
{{prunedCount}} |
||||
</div> |
||||
<div class="pruning__text-sub"> |
||||
{{#unless filesOnly}} |
||||
{{_ "messages_pruned"}} |
||||
{{else}} |
||||
{{_ "files_pruned"}} |
||||
{{/unless}} |
||||
</div> |
||||
</div> |
||||
</main> |
||||
{{else}} |
||||
<main class="rc-user-info__pruning"> |
||||
<p class="pruning__header">{{_ "Prune_finished"}}</p> |
||||
<div class="pruning-wrapper prune__finished"> |
||||
{{> icon icon="check" block="rc-icon"}} |
||||
<div class="pruning__text"> |
||||
{{prunedCount}} |
||||
</div> |
||||
<div class="pruning__text-sub"> |
||||
{{#unless filesOnly}} |
||||
{{#if (isSingular prunedCount)}} |
||||
{{_ "message_pruned"}} |
||||
{{else}} |
||||
{{_ "messages_pruned"}} |
||||
{{/if}} |
||||
{{else}} |
||||
{{#if (isSingular prunedCount)}} |
||||
{{_ "file_pruned"}} |
||||
{{else}} |
||||
{{_ "files_pruned"}} |
||||
{{/if}} |
||||
{{/unless}} |
||||
</div> |
||||
</div> |
||||
</main> |
||||
{{/unless}} |
||||
{{/unless}} |
||||
</template> |
||||
@ -1,351 +0,0 @@ |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import { Blaze } from 'meteor/blaze'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Session } from 'meteor/session'; |
||||
import { Template } from 'meteor/templating'; |
||||
import moment from 'moment'; |
||||
|
||||
import { ChatRoom } from '../../../models'; |
||||
import { t, roomTypes } from '../../../utils'; |
||||
import { settings } from '../../../settings'; |
||||
import { modal, call } from '../../../ui-utils'; |
||||
import { AutoComplete } from '../../../meteor-autocomplete/client'; |
||||
|
||||
const getRoomName = function() { |
||||
const room = ChatRoom.findOne(Session.get('openedRoom')); |
||||
if (!room) { |
||||
return; |
||||
} |
||||
if (room.name) { |
||||
return `#${ room.name }`; |
||||
} |
||||
|
||||
return t('conversation_with_s', roomTypes.getRoomName(room.t, room)); |
||||
}; |
||||
|
||||
const purgeWorker = function(roomId, oldest, latest, inclusive, limit, excludePinned, ignoreDiscussion, filesOnly, fromUsers, ignoreThreads) { |
||||
return call('cleanRoomHistory', { |
||||
roomId, |
||||
latest, |
||||
oldest, |
||||
inclusive, |
||||
limit, |
||||
excludePinned, |
||||
ignoreDiscussion, |
||||
filesOnly, |
||||
fromUsers, |
||||
ignoreThreads, |
||||
}); |
||||
}; |
||||
|
||||
|
||||
const getTimeZoneOffset = function() { |
||||
const offset = new Date().getTimezoneOffset(); |
||||
const absOffset = Math.abs(offset); |
||||
return `${ offset < 0 ? '+' : '-' }${ `00${ Math.floor(absOffset / 60) }`.slice(-2) }:${ `00${ absOffset % 60 }`.slice(-2) }`; |
||||
}; |
||||
|
||||
|
||||
const filterNames = (old) => { |
||||
const reg = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`); |
||||
return [...old.replace(' ', '').toLocaleLowerCase()].filter((f) => reg.test(f)).join(''); |
||||
}; |
||||
|
||||
Template.cleanHistory.helpers({ |
||||
roomId() { |
||||
const room = ChatRoom.findOne(Session.get('openedRoom')); |
||||
return room && room._id; |
||||
}, |
||||
roomName() { |
||||
return getRoomName(); |
||||
}, |
||||
warningBox() { |
||||
return Template.instance().warningBox.get(); |
||||
}, |
||||
validate() { |
||||
return Template.instance().validate.get(); |
||||
}, |
||||
filesOnly() { |
||||
return Template.instance().cleanHistoryFilesOnly.get(); |
||||
}, |
||||
busy() { |
||||
return Template.instance().cleanHistoryBusy.get(); |
||||
}, |
||||
finished() { |
||||
return Template.instance().cleanHistoryFinished.get(); |
||||
}, |
||||
prunedCount() { |
||||
return Template.instance().cleanHistoryPrunedCount.get(); |
||||
}, |
||||
config() { |
||||
const filter = Template.instance().userFilter; |
||||
return { |
||||
filter: filter.get(), |
||||
noMatchTemplate: 'userSearchEmpty', |
||||
modifier(text) { |
||||
const f = filter.get(); |
||||
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), function(part) { |
||||
return `<strong>${ part }</strong>`; |
||||
}) }`;
|
||||
}, |
||||
}; |
||||
}, |
||||
selectedUsers() { |
||||
return Template.instance().selectedUsers.get(); |
||||
}, |
||||
autocomplete(key) { |
||||
const instance = Template.instance(); |
||||
const param = instance.ac[key]; |
||||
return typeof param === 'function' ? param.apply(instance.ac) : param; |
||||
}, |
||||
items() { |
||||
return Template.instance().ac.filteredList(); |
||||
}, |
||||
isSingular(prunedCount) { |
||||
return prunedCount === 1; |
||||
}, |
||||
}); |
||||
|
||||
Template.cleanHistory.onCreated(function() { |
||||
this.warningBox = new ReactiveVar(''); |
||||
this.validate = new ReactiveVar(''); |
||||
this.selectedUsers = new ReactiveVar([]); |
||||
this.userFilter = new ReactiveVar(''); |
||||
|
||||
this.cleanHistoryFromDate = new ReactiveVar(''); |
||||
this.cleanHistoryFromTime = new ReactiveVar(''); |
||||
this.cleanHistoryToDate = new ReactiveVar(''); |
||||
this.cleanHistoryToTime = new ReactiveVar(''); |
||||
this.cleanHistorySelectedUsers = new ReactiveVar([]); |
||||
this.cleanHistoryInclusive = new ReactiveVar(false); |
||||
this.cleanHistoryExcludePinned = new ReactiveVar(false); |
||||
this.cleanHistoryFilesOnly = new ReactiveVar(false); |
||||
|
||||
this.ignoreDiscussion = new ReactiveVar(false); |
||||
this.ignoreThreads = new ReactiveVar(false); |
||||
|
||||
this.cleanHistoryBusy = new ReactiveVar(false); |
||||
this.cleanHistoryFinished = new ReactiveVar(false); |
||||
this.cleanHistoryPrunedCount = new ReactiveVar(0); |
||||
|
||||
this.ac = new AutoComplete( |
||||
{ |
||||
selector: { |
||||
item: '.rc-popup-list__item', |
||||
container: '.rc-popup-list__list', |
||||
}, |
||||
|
||||
limit: 10, |
||||
inputDelay: 300, |
||||
rules: [ |
||||
{ |
||||
collection: 'UserAndRoom', |
||||
endpoint: 'users.autocomplete', |
||||
field: 'username', |
||||
matchAll: true, |
||||
doNotChangeWidth: false, |
||||
selector(match) { |
||||
return { term: match }; |
||||
}, |
||||
sort: 'username', |
||||
}, |
||||
], |
||||
|
||||
}); |
||||
this.ac.tmplInst = this; |
||||
}); |
||||
|
||||
Template.cleanHistory.onRendered(function() { |
||||
const users = this.selectedUsers; |
||||
const selUsers = this.cleanHistorySelectedUsers; |
||||
|
||||
this.ac.element = this.firstNode.parentElement.querySelector('[name="users"]'); |
||||
this.ac.$element = $(this.ac.element); |
||||
this.ac.$element.on('autocompleteselect', function(e, { item }) { |
||||
const usersArr = users.get(); |
||||
usersArr.push(item); |
||||
users.set(usersArr); |
||||
selUsers.set(usersArr); |
||||
}); |
||||
|
||||
Tracker.autorun(() => { |
||||
const metaFromDate = this.cleanHistoryFromDate.get(); |
||||
const metaFromTime = this.cleanHistoryFromTime.get(); |
||||
const metaToDate = this.cleanHistoryToDate.get(); |
||||
const metaToTime = this.cleanHistoryToTime.get(); |
||||
const metaSelectedUsers = this.cleanHistorySelectedUsers.get(); |
||||
const metaCleanHistoryExcludePinned = this.cleanHistoryExcludePinned.get(); |
||||
const metaCleanHistoryFilesOnly = this.cleanHistoryFilesOnly.get(); |
||||
|
||||
let fromDate = new Date('0001-01-01T00:00:00Z'); |
||||
let toDate = new Date('9999-12-31T23:59:59Z'); |
||||
|
||||
if (metaFromDate) { |
||||
fromDate = new Date(`${ metaFromDate }T${ metaFromTime || '00:00' }:00${ getTimeZoneOffset() }`); |
||||
} |
||||
|
||||
if (metaToDate) { |
||||
toDate = new Date(`${ metaToDate }T${ metaToTime || '00:00' }:00${ getTimeZoneOffset() }`); |
||||
} |
||||
|
||||
const exceptPinned = metaCleanHistoryExcludePinned ? ` ${ t('except_pinned', {}) }` : ''; |
||||
const ifFrom = metaSelectedUsers.length ? ` ${ t('if_they_are_from', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [metaSelectedUsers.map((element) => element.username).join(', ')], |
||||
}) }` : '';
|
||||
const filesOrMessages = t(metaCleanHistoryFilesOnly ? 'files' : 'messages', {}); |
||||
|
||||
if (metaFromDate && metaToDate) { |
||||
this.warningBox.set(t('Prune_Warning_between', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [filesOrMessages, getRoomName(), moment(fromDate).format('L LT'), moment(toDate).format('L LT')], |
||||
}) + exceptPinned + ifFrom); |
||||
} else if (metaFromDate) { |
||||
this.warningBox.set(t('Prune_Warning_after', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [filesOrMessages, getRoomName(), moment(fromDate).format('L LT')], |
||||
}) + exceptPinned + ifFrom); |
||||
} else if (metaToDate) { |
||||
this.warningBox.set(t('Prune_Warning_before', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [filesOrMessages, getRoomName(), moment(toDate).format('L LT')], |
||||
}) + exceptPinned + ifFrom); |
||||
} else { |
||||
this.warningBox.set(t('Prune_Warning_all', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [filesOrMessages, getRoomName()], |
||||
}) + exceptPinned + ifFrom); |
||||
} |
||||
|
||||
if (fromDate > toDate) { |
||||
return this.validate.set(t('Newer_than_may_not_exceed_Older_than', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [], |
||||
})); |
||||
} |
||||
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) { |
||||
return this.validate.set(t('error-invalid-date', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [], |
||||
})); |
||||
} |
||||
this.validate.set(''); |
||||
}); |
||||
}); |
||||
|
||||
Template.cleanHistory.events({ |
||||
'change [name=from__date]'(e, instance) { |
||||
instance.cleanHistoryFromDate.set(e.target.value); |
||||
}, |
||||
'change [name=from__time]'(e, instance) { |
||||
instance.cleanHistoryFromTime.set(e.target.value); |
||||
}, |
||||
'change [name=to__date]'(e, instance) { |
||||
instance.cleanHistoryToDate.set(e.target.value); |
||||
}, |
||||
'change [name=to__time]'(e, instance) { |
||||
instance.cleanHistoryToTime.set(e.target.value); |
||||
}, |
||||
'change [name=inclusive]'(e, instance) { |
||||
instance.cleanHistoryInclusive.set(e.target.checked); |
||||
}, |
||||
'change [name=excludePinned]'(e, instance) { |
||||
instance.cleanHistoryExcludePinned.set(e.target.checked); |
||||
}, |
||||
'change [name=filesOnly]'(e, instance) { |
||||
instance.cleanHistoryFilesOnly.set(e.target.checked); |
||||
}, |
||||
'change [name=ignoreDiscussion]'(e, instance) { |
||||
instance.ignoreDiscussion.set(e.target.checked); |
||||
}, |
||||
'change [name=ignoreThreads]'(e, instance) { |
||||
instance.ignoreThreads.set(e.target.checked); |
||||
}, |
||||
'click .js-prune'(e, instance) { |
||||
modal.open({ |
||||
title: t('Are_you_sure'), |
||||
text: t('Prune_Modal'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes_prune_them'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, async function() { |
||||
instance.cleanHistoryBusy.set(true); |
||||
const metaFromDate = instance.cleanHistoryFromDate.get(); |
||||
const metaFromTime = instance.cleanHistoryFromTime.get(); |
||||
const metaToDate = instance.cleanHistoryToDate.get(); |
||||
const metaToTime = instance.cleanHistoryToTime.get(); |
||||
const metaSelectedUsers = instance.cleanHistorySelectedUsers.get(); |
||||
const metaCleanHistoryInclusive = instance.cleanHistoryInclusive.get(); |
||||
const metaCleanHistoryExcludePinned = instance.cleanHistoryExcludePinned.get(); |
||||
const metaCleanHistoryFilesOnly = instance.cleanHistoryFilesOnly.get(); |
||||
const ignoreDiscussion = instance.ignoreDiscussion.get(); |
||||
const ignoreThreads = instance.ignoreThreads.get(); |
||||
|
||||
let fromDate = new Date('0001-01-01T00:00:00Z'); |
||||
let toDate = new Date('9999-12-31T23:59:59Z'); |
||||
|
||||
if (metaFromDate) { |
||||
fromDate = new Date(`${ metaFromDate }T${ metaFromTime || '00:00' }:00${ getTimeZoneOffset() }`); |
||||
} |
||||
|
||||
if (metaToDate) { |
||||
toDate = new Date(`${ metaToDate }T${ metaToTime || '00:00' }:00${ getTimeZoneOffset() }`); |
||||
} |
||||
|
||||
const roomId = Session.get('openedRoom'); |
||||
const users = metaSelectedUsers.map((element) => element.username); |
||||
const limit = 2000; |
||||
let count = 0; |
||||
let result; |
||||
do { |
||||
result = await purgeWorker(roomId, fromDate, toDate, metaCleanHistoryInclusive, limit, metaCleanHistoryExcludePinned, ignoreDiscussion, metaCleanHistoryFilesOnly, users, ignoreThreads); // eslint-disable-line no-await-in-loop
|
||||
count += result; |
||||
} while (result === limit); |
||||
|
||||
instance.cleanHistoryPrunedCount.set(count); |
||||
instance.cleanHistoryFinished.set(true); |
||||
}); |
||||
}, |
||||
'click .rc-input--usernames .rc-tags__tag'({ target }, t) { |
||||
const { username } = Blaze.getData(target); |
||||
t.selectedUsers.set(t.selectedUsers.get().filter((user) => user.username !== username)); |
||||
t.cleanHistorySelectedUsers.set(t.selectedUsers.get()); |
||||
}, |
||||
'click .rc-popup-list__item'(e, t) { |
||||
t.ac.onItemClick(this, e); |
||||
}, |
||||
'input [name="users"]'(e, t) { |
||||
const input = e.target; |
||||
const position = input.selectionEnd || input.selectionStart; |
||||
const { length } = input.value; |
||||
const modified = filterNames(input.value); |
||||
input.value = modified; |
||||
document.activeElement === input && e && /input/i.test(e.type) && (input.selectionEnd = position + input.value.length - length); |
||||
|
||||
t.userFilter.set(modified); |
||||
}, |
||||
'keydown [name="users"]'(e, t) { |
||||
if ([8, 46].includes(e.keyCode) && e.target.value === '') { |
||||
const users = t.selectedUsers; |
||||
const usersArr = users.get(); |
||||
usersArr.pop(); |
||||
t.cleanHistorySelectedUsers.set(usersArr); |
||||
return users.set(usersArr); |
||||
} |
||||
|
||||
t.ac.onKeyDown(e); |
||||
}, |
||||
'keyup [name="users"]'(e, t) { |
||||
t.ac.onKeyUp(e); |
||||
}, |
||||
'focus [name="users"]'(e, t) { |
||||
t.ac.onFocus(e); |
||||
}, |
||||
'blur [name="users"]'(e, t) { |
||||
t.ac.onBlur(e); |
||||
}, |
||||
}); |
||||
@ -1,59 +0,0 @@ |
||||
.rc-datetime__left { |
||||
display: inline-block; |
||||
|
||||
width: 52%; |
||||
} |
||||
|
||||
.rc-datetime__right { |
||||
display: inline-block; |
||||
|
||||
width: calc(48% - 0.3rem); |
||||
} |
||||
|
||||
.rc-user-info__pruning { |
||||
position: absolute; |
||||
top: 50%; |
||||
left: 50%; |
||||
|
||||
transform: translate(-50%, calc(-50% - 2rem)); |
||||
} |
||||
|
||||
.pruning__header { |
||||
text-align: center; |
||||
|
||||
font-weight: 900; |
||||
} |
||||
|
||||
.pruning-wrapper { |
||||
text-align: center; |
||||
|
||||
color: var(--rc-color-link-active); |
||||
|
||||
&.prune__finished { |
||||
color: #12c212; |
||||
} |
||||
|
||||
& .rc-icon--loading { |
||||
width: 16rem; |
||||
height: 16rem; |
||||
margin: 1rem 0; |
||||
|
||||
animation: spin 2s linear infinite; |
||||
} |
||||
|
||||
& .rc-icon--check { |
||||
font-size: 1rem; |
||||
} |
||||
|
||||
& .pruning__text { |
||||
margin-top: -17rem; |
||||
|
||||
font-size: 3.5em; |
||||
|
||||
line-height: 16rem; |
||||
} |
||||
|
||||
& .pruning__text-sub { |
||||
margin-top: calc(-8rem + 1.5em); |
||||
} |
||||
} |
||||
@ -0,0 +1,319 @@ |
||||
import React, { useCallback, useEffect, useState } from 'react'; |
||||
import { Field, ButtonGroup, Button, CheckBox, InputBox, Box, Margins, Callout } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; |
||||
import moment from 'moment'; |
||||
|
||||
import UserAutoCompleteMultiple from '../../../../../ee/client/audit/UserAutoCompleteMultiple'; |
||||
import { useTranslation } from '../../../../contexts/TranslationContext'; |
||||
import VerticalBar from '../../../../components/VerticalBar'; |
||||
import { useUserRoom } from '../../../../contexts/UserContext'; |
||||
import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext'; |
||||
import { useSetModal } from '../../../../contexts/ModalContext'; |
||||
import { useForm } from '../../../../hooks/useForm'; |
||||
import { useMethod } from '../../../../contexts/ServerContext'; |
||||
import DeleteWarningModal from '../../../../components/DeleteWarningModal'; |
||||
|
||||
const getTimeZoneOffset = function() { |
||||
const offset = new Date().getTimezoneOffset(); |
||||
const absOffset = Math.abs(offset); |
||||
return `${ offset < 0 ? '+' : '-' }${ `00${ Math.floor(absOffset / 60) }`.slice(-2) }:${ `00${ absOffset % 60 }`.slice(-2) }`; |
||||
}; |
||||
|
||||
export const DialogPruneMessages = ({ children, ...props }) => |
||||
<DeleteWarningModal {...props}><Box textAlign='center' fontScale='s1'>{children}</Box></DeleteWarningModal>; |
||||
|
||||
export const DateTimeRow = ({ |
||||
label, |
||||
dateTime, |
||||
handleDateTime, |
||||
}) => ( |
||||
<Field > |
||||
<Field.Label flexGrow={0}>{label}</Field.Label> |
||||
<Box display='flex' mi='neg-x4'> |
||||
<Margins inline='x4'> |
||||
<InputBox type='date' value={dateTime?.date} onChange={handleDateTime?.date} flexGrow={1} h='x20'/> |
||||
<InputBox type='time' value={dateTime?.time} onChange={handleDateTime?.time} flexGrow={1} h='x20'/> |
||||
</Margins> |
||||
</Box> |
||||
</Field> |
||||
); |
||||
|
||||
export const PruneMessages = ({ |
||||
callOutText, |
||||
validateText, |
||||
newerDateTime, |
||||
handleNewerDateTime, |
||||
olderDateTime, |
||||
handleOlderDateTime, |
||||
users, |
||||
inclusive, |
||||
pinned, |
||||
discussion, |
||||
threads, |
||||
attached, |
||||
handleInclusive, |
||||
handlePinned, |
||||
handleDiscussion, |
||||
handleThreads, |
||||
handleAttached, |
||||
onClickClose, |
||||
onClickPrune, |
||||
onChangeUsers, |
||||
}) => { |
||||
const t = useTranslation(); |
||||
|
||||
const inclusiveCheckboxId = useUniqueId(); |
||||
const pinnedCheckboxId = useUniqueId(); |
||||
const discussionCheckboxId = useUniqueId(); |
||||
const threadsCheckboxId = useUniqueId(); |
||||
const attachedCheckboxId = useUniqueId(); |
||||
|
||||
return ( |
||||
<> |
||||
<VerticalBar.Header> |
||||
<VerticalBar.Icon name='eraser' /> |
||||
<VerticalBar.Text>{t('Prune_Messages')}</VerticalBar.Text> |
||||
{onClickClose && <VerticalBar.Close onClick={onClickClose} />} |
||||
</VerticalBar.Header> |
||||
<VerticalBar.ScrollableContent> |
||||
<DateTimeRow label={t('Newer_than')} dateTime={newerDateTime} handleDateTime={handleNewerDateTime} /> |
||||
<DateTimeRow label={t('Older_than')} dateTime={olderDateTime} handleDateTime={handleOlderDateTime} /> |
||||
|
||||
<Field > |
||||
<Field.Label flexGrow={0}>{t('Only_from_users')}</Field.Label> |
||||
<UserAutoCompleteMultiple value={users} onChange={onChangeUsers} placeholder={t('Please_enter_usernames')} /> |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Field.Row> |
||||
<CheckBox id={inclusiveCheckboxId} checked={inclusive} onChange={handleInclusive} /> |
||||
<Field.Label htmlFor={inclusiveCheckboxId}>{t('Inclusive')}</Field.Label> |
||||
</Field.Row> |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Field.Row> |
||||
<CheckBox id={pinnedCheckboxId} checked={pinned} onChange={handlePinned} /> |
||||
<Field.Label htmlFor={pinnedCheckboxId}>{t('RetentionPolicy_DoNotPrunePinned')}</Field.Label> |
||||
</Field.Row> |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Field.Row> |
||||
<CheckBox id={discussionCheckboxId} checked={discussion} onChange={handleDiscussion} /> |
||||
<Field.Label htmlFor={discussionCheckboxId}>{t('RetentionPolicy_DoNotPruneDiscussion')}</Field.Label> |
||||
</Field.Row> |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Field.Row> |
||||
<CheckBox id={threadsCheckboxId} checked={threads} onChange={handleThreads} /> |
||||
<Field.Label htmlFor={threadsCheckboxId}>{t('RetentionPolicy_DoNotPruneThreads')}</Field.Label> |
||||
</Field.Row> |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Field.Row> |
||||
<CheckBox id={attachedCheckboxId} checked={attached} onChange={handleAttached} /> |
||||
<Field.Label htmlFor={attachedCheckboxId}>{t('Files_only')}</Field.Label> |
||||
</Field.Row> |
||||
</Field> |
||||
|
||||
{callOutText && !validateText && <Callout type='warning'>{callOutText}</Callout>} |
||||
{validateText && <Callout type='warning'>{validateText}</Callout>} |
||||
</VerticalBar.ScrollableContent> |
||||
<VerticalBar.Footer> |
||||
<ButtonGroup stretch> |
||||
<Button primary danger disabled={validateText && true} onClick={onClickPrune}>{t('Prune')}</Button> |
||||
</ButtonGroup> |
||||
</VerticalBar.Footer> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
const initialValues = { |
||||
newerDate: '', |
||||
newerTime: '', |
||||
olderDate: '', |
||||
olderTime: '', |
||||
users: [], |
||||
inclusive: false, |
||||
pinned: false, |
||||
discussion: false, |
||||
threads: false, |
||||
attached: false, |
||||
}; |
||||
|
||||
export default ({ |
||||
rid, |
||||
tabBar, |
||||
}) => { |
||||
const t = useTranslation(); |
||||
const room = useUserRoom(rid); |
||||
room.type = room.t; |
||||
room.rid = rid; |
||||
const { name } = room; |
||||
|
||||
const setModal = useSetModal(); |
||||
const onClickClose = useMutableCallback(() => tabBar && tabBar.close()); |
||||
const closeModal = useCallback(() => setModal(null), [setModal]); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const pruneMessages = useMethod('cleanRoomHistory'); |
||||
|
||||
const [fromDate, setFromDate] = useState(new Date('0001-01-01T00:00:00Z')); |
||||
const [toDate, setToDate] = useState(new Date('9999-12-31T23:59:59Z')); |
||||
const [callOutText, setCallOutText] = useState(); |
||||
const [validateText, setValidateText] = useState(); |
||||
const [count, setCount] = useState(0); |
||||
|
||||
const { values, handlers, reset } = useForm(initialValues); |
||||
const { |
||||
newerDate, |
||||
newerTime, |
||||
olderDate, |
||||
olderTime, |
||||
users, |
||||
inclusive, |
||||
pinned, |
||||
discussion, |
||||
threads, |
||||
attached, |
||||
} = values; |
||||
|
||||
const { |
||||
handleNewerDate, |
||||
handleNewerTime, |
||||
handleOlderDate, |
||||
handleOlderTime, |
||||
handleUsers, |
||||
handleInclusive, |
||||
handlePinned, |
||||
handleDiscussion, |
||||
handleThreads, |
||||
handleAttached, |
||||
} = handlers; |
||||
|
||||
const onChangeUsers = useMutableCallback((value, action) => { |
||||
if (!action) { |
||||
if (users.includes(value)) { |
||||
return; |
||||
} |
||||
return handleUsers([...users, value]); |
||||
} |
||||
handleUsers(users.filter((current) => current !== value)); |
||||
}); |
||||
|
||||
const handlePrune = useMutableCallback(async () => { |
||||
const limit = 2000; |
||||
let result; |
||||
|
||||
try { |
||||
if (count === limit) { |
||||
return; |
||||
} |
||||
|
||||
result = await pruneMessages({ roomId: rid, latest: toDate, oldest: fromDate, inclusive, limit, excludePinned: pinned, ignoreDiscussion: discussion, filesOnly: attached, fromUsers: users, ignoreThreads: threads }); |
||||
setCount(result); |
||||
|
||||
if (result < 1) { |
||||
throw new Error(t('No_messages_found_to_prune')); |
||||
} |
||||
|
||||
dispatchToastMessage({ type: 'success', message: `${ result } ${ t('messages_pruned') }` }); |
||||
closeModal(); |
||||
reset(); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
closeModal(); |
||||
} |
||||
}); |
||||
|
||||
const handleModal = () => { |
||||
setModal(<DialogPruneMessages |
||||
onCancel={closeModal} |
||||
onDelete={handlePrune} |
||||
deleteText={t('Yes_prune_them')} |
||||
>{t('Prune_Modal')}</DialogPruneMessages>); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
if (newerDate) { |
||||
setFromDate(new Date(`${ newerDate }T${ newerTime || '00:00' }:00${ getTimeZoneOffset() }`)); |
||||
} |
||||
|
||||
if (olderDate) { |
||||
setToDate(new Date(`${ olderDate }T${ olderTime || '24:00' }:00${ getTimeZoneOffset() }`)); |
||||
} |
||||
}, [newerDate, newerTime, olderDate, olderTime]); |
||||
|
||||
useEffect(() => { |
||||
const exceptPinned = pinned ? ` ${ t('except_pinned', {}) }` : ''; |
||||
const ifFrom = users.length ? ` ${ t('if_they_are_from', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [users.map((element) => element).join(', ')], |
||||
}) }` : '';
|
||||
const filesOrMessages = t(attached ? 'files' : 'messages', {}); |
||||
|
||||
if (newerDate && olderDate) { |
||||
setCallOutText(t('Prune_Warning_between', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [filesOrMessages, name, moment(fromDate).format('L LT'), moment(toDate).format('L LT')], |
||||
}) + exceptPinned + ifFrom); |
||||
} else if (newerDate) { |
||||
setCallOutText(t('Prune_Warning_after', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [filesOrMessages, name, moment(fromDate).format('L LT')], |
||||
}) + exceptPinned + ifFrom); |
||||
} else if (olderDate) { |
||||
setCallOutText(t('Prune_Warning_before', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [filesOrMessages, name, moment(toDate).format('L LT')], |
||||
}) + exceptPinned + ifFrom); |
||||
} else { |
||||
setCallOutText(t('Prune_Warning_all', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [filesOrMessages, name], |
||||
}) + exceptPinned + ifFrom); |
||||
} |
||||
|
||||
if (fromDate > toDate) { |
||||
return setValidateText(t('Newer_than_may_not_exceed_Older_than', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [], |
||||
})); |
||||
} |
||||
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) { |
||||
return setValidateText(t('error-invalid-date', { |
||||
postProcess: 'sprintf', |
||||
sprintf: [], |
||||
})); |
||||
} |
||||
|
||||
setValidateText(); |
||||
}, [newerDate, olderDate, fromDate, toDate, attached, name, t, pinned, users]); |
||||
|
||||
return ( |
||||
<PruneMessages |
||||
callOutText={callOutText} |
||||
validateText={validateText} |
||||
newerDateTime={{ date: newerDate, time: newerTime }} |
||||
handleNewerDateTime={{ date: handleNewerDate, time: handleNewerTime }} |
||||
olderDateTime={{ date: olderDate, time: olderTime }} |
||||
handleOlderDateTime={{ date: handleOlderDate, time: handleOlderTime }} |
||||
users={users} |
||||
inclusive={inclusive} |
||||
pinned={pinned} |
||||
discussion={discussion} |
||||
threads={threads} |
||||
attached={attached} |
||||
handleInclusive={handleInclusive} |
||||
handlePinned={handlePinned} |
||||
handleDiscussion={handleDiscussion} |
||||
handleThreads={handleThreads} |
||||
handleAttached={handleAttached} |
||||
onClickClose={onClickClose} |
||||
onClickPrune={handleModal} |
||||
onChangeUsers={onChangeUsers} |
||||
/> |
||||
); |
||||
}; |
||||
@ -0,0 +1,23 @@ |
||||
import React from 'react'; |
||||
|
||||
import { PruneMessages } from './PruneMessages'; |
||||
import VerticalBar from '../../../../components/VerticalBar'; |
||||
|
||||
export default { |
||||
title: 'components/PruneMessages', |
||||
component: PruneMessages, |
||||
}; |
||||
|
||||
export const Default = () => <VerticalBar> |
||||
<PruneMessages |
||||
onClickClose={alert} |
||||
/> |
||||
</VerticalBar>; |
||||
|
||||
export const withCallout = () => <VerticalBar> |
||||
<PruneMessages |
||||
onClickClose={alert} |
||||
pinned={true} |
||||
callOutText='Lorem Ipsum Ipsum Ipsum' |
||||
/> |
||||
</VerticalBar>; |
||||
@ -0,0 +1,3 @@ |
||||
import PruneMessages from './PruneMessages'; |
||||
|
||||
export default PruneMessages; |
||||
Loading…
Reference in new issue