Task level filtering working

pull/1297/head
Piero Toffanin 2023-03-14 11:10:38 -04:00
rodzic c90b575850
commit 8c16f9a26d
6 zmienionych plików z 165 dodań i 27 usunięć

Wyświetl plik

@ -49,10 +49,10 @@ class ProjectFilter(filters.FilterSet):
tag_pattern = re.compile("#[^\s]+")
tags = set(re.findall(tag_pattern, value))
deep_tags = set([t for t in tags if t.startswith("##")])
project_tags = tags - deep_tags
task_tags = set([t for t in tags if t.startswith("##")])
project_tags = tags - task_tags
deep_tags = [t.replace("##", "") for t in deep_tags]
task_tags = [t.replace("##", "") for t in task_tags]
project_tags = [t.replace("#", "") for t in project_tags]
names = re.sub("\s+", " ", re.sub(tag_pattern, "", value)).strip()
@ -63,13 +63,12 @@ class ProjectFilter(filters.FilterSet):
name_query = SearchQuery(names, search_type="plain")
qs = qs.annotate(n_search=project_name_vec + task_name_vec).filter(n_search=name_query)
if len(deep_tags) > 0:
project_tags_vec = SearchVector("tags")
task_tags_vec = SearchVector(StringAgg("task__tags", delimiter=' '))
tags_query = SearchQuery(deep_tags[0])
for t in deep_tags[1:]:
if len(task_tags) > 0:
task_tags_vec = SearchVector("task__tags")
tags_query = SearchQuery(task_tags[0])
for t in task_tags[1:]:
tags_query = tags_query & SearchQuery(t)
qs = qs.annotate(dt_search=project_tags_vec + task_tags_vec).filter(dt_search=tags_query)
qs = qs.annotate(tt_search=task_tags_vec).filter(tt_search=tags_query)
if len(project_tags) > 0:
project_tags_vec = SearchVector("tags")

Wyświetl plik

@ -122,6 +122,8 @@ class Paginator extends React.Component {
ref={(domNode) => { this.searchInput = domNode}}
className="form-control search theme-border-secondary-07"
placeholder={_("Search names or #tags")}
spellCheck="false"
autoComplete="false"
value={searchText}
onKeyDown={this.handleSearchKeyDown}
onChange={this.handleSearchChange} />

Wyświetl plik

@ -41,7 +41,9 @@ class ProjectListItem extends React.Component {
importing: false,
buttons: [],
sortKey: "-created_at",
filterTags: []
filterTags: [],
selectedTags: [],
filterText: ""
};
this.sortItems = [{
@ -90,6 +92,13 @@ class ProjectListItem extends React.Component {
if (this.refreshRequest) this.refreshRequest.abort();
}
componentDidUpdate(prevProps, prevState){
if (prevState.filterText !== this.state.filterText ||
prevState.selectedTags.length !== this.state.selectedTags.length){
if (this.taskList) this.taskList.applyFilter(this.state.filterText, this.state.selectedTags);
}
}
getDefaultUploadState(){
return {
uploading: false,
@ -500,7 +509,42 @@ class ProjectListItem extends React.Component {
}
tagsChanged = (filterTags) => {
this.setState({filterTags});
this.setState({filterTags, selectedTags: []});
}
handleFilterTextChange = e => {
this.setState({filterText: e.target.value});
}
toggleTag = t => {
return () => {
if (this.state.selectedTags.indexOf(t) === -1){
this.setState(update(this.state, { selectedTags: {$push: [t]} }));
}else{
this.setState({selectedTags: this.state.selectedTags.filter(tag => tag !== t)});
}
}
}
selectTag = t => {
if (this.state.selectedTags.indexOf(t) === -1){
this.setState(update(this.state, { selectedTags: {$push: [t]} }));
}
}
clearFilter = () => {
this.setState({
filterText: "",
selectedTags: []
});
}
onOpenFilter = () => {
if (this.state.filterTags.length === 0){
setTimeout(() => {
this.filterTextInput.focus();
}, 0);
}
}
render() {
@ -585,17 +629,35 @@ class ProjectListItem extends React.Component {
{this.state.showTaskList && numTasks > 1 ?
<div className="task-filters">
{filterTags.length > 0 ?
<div className="btn-group">
<i className='fa fa-filter'></i>
<a href="javascript:void(0);" className="dropdown-toggle" data-toggle-outside data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{_("Filter")}
</a>
<ul className="dropdown-menu dropdown-menu-right">
{filterTags.map(t => <li key={t}>{t}</li>)}
</ul>
</div>
: ""}
<div className="btn-group">
{this.state.selectedTags.length || this.state.filterText !== "" ?
<a className="quick-clear-filter" href="javascript:void(0)" onClick={this.clearFilter}>×</a>
: ""}
<i className='fa fa-filter'></i>
<a href="javascript:void(0);" onClick={this.onOpenFilter} className="dropdown-toggle" data-toggle-outside data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{_("Filter")}
</a>
<ul className="dropdown-menu dropdown-menu-right filter-dropdown">
<li className="filter-text-container">
<input type="text" className="form-control filter-text theme-border-secondary-07"
value={this.state.filterText}
ref={domNode => {this.filterTextInput = domNode}}
placeholder=""
spellCheck="false"
autoComplete="false"
onChange={this.handleFilterTextChange} />
</li>
{filterTags.map(t => <li key={t} className="tag-selection">
<input type="checkbox"
className="filter-checkbox"
id={"filter-tag-" + data.id + "-" + t}
checked={this.state.selectedTags.indexOf(t) !== -1}
onChange={this.toggleTag(t)} /> <label className="filter-checkbox-label" htmlFor={"filter-tag-" + data.id + "-" + t}>{t}</label>
</li>)}
<li className="clear-container"><input type="button" onClick={this.clearFilter} className="btn btn-default btn-xs" value={_("Clear")}/></li>
</ul>
</div>
<div className="btn-group">
<i className='fa fa-sort-alpha-down'></i>
<a href="javascript:void(0);" className="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@ -658,6 +720,7 @@ class ProjectListItem extends React.Component {
onTaskMoved={this.taskMoved}
hasPermission={this.hasPermission}
onTagsChanged={this.tagsChanged}
onTagClicked={this.selectTag}
history={this.props.history}
/> : ""}

Wyświetl plik

@ -12,7 +12,8 @@ class TaskList extends React.Component {
onDelete: PropTypes.func,
onTaskMoved: PropTypes.func,
hasPermission: PropTypes.func.isRequired,
onTagsChanged: PropTypes.func
onTagsChanged: PropTypes.func,
onTagClicked: PropTypes.func
}
constructor(props){
@ -21,7 +22,9 @@ class TaskList extends React.Component {
this.state = {
tasks: [],
error: "",
loading: true
loading: true,
filterText: "",
filterTags: []
};
this.refresh = this.refresh.bind(this);
@ -42,6 +45,10 @@ class TaskList extends React.Component {
this.refresh();
}
applyFilter(text, tags){
this.setState({filterText: text, filterTags: tags});
}
loadTaskList(){
this.setState({loading: true});
@ -112,6 +119,17 @@ class TaskList extends React.Component {
setTimeout(() => this.notifyTagsChanged(), 0);
}
arrayContainsAll = (a, b) => {
let miss = false;
for (let i = 0; i < b.length; i++){
if (a.indexOf(b[i]) === -1){
miss = true;
break;
}
}
return !miss;
}
render() {
let message = "";
if (this.state.loading){
@ -124,7 +142,10 @@ class TaskList extends React.Component {
return (
<div className="task-list">
{this.state.tasks.map(task => (
{this.state.tasks.filter(t => {
return t.name.toLocaleLowerCase().indexOf(this.state.filterText.toLocaleLowerCase()) !== -1 &&
this.arrayContainsAll(t.tags, this.state.filterTags);
}).map(task => (
<TaskListItem
data={task}
key={task.id}
@ -133,6 +154,7 @@ class TaskList extends React.Component {
onMove={this.moveTask}
onDuplicate={this.refresh}
onEdited={this.taskEdited}
onTagClicked={this.props.onTagClicked}
hasPermission={this.props.hasPermission}
history={this.props.history} />
))}

Wyświetl plik

@ -25,7 +25,8 @@ class TaskListItem extends React.Component {
onMove: PropTypes.func,
onDuplicate: PropTypes.func,
hasPermission: PropTypes.func,
onEdited: PropTypes.func
onEdited: PropTypes.func,
onTagClicked: PropTypes.func
}
constructor(props){
@ -404,6 +405,12 @@ class TaskListItem extends React.Component {
}else return false;
}
handleTagClick = t => {
return () => {
if (this.props.onTagClicked) this.props.onTagClicked(t);
}
}
render() {
const task = this.state.task;
const name = task.name !== null ? task.name : interpolate(_("Task #%(number)s"), { number: task.id });
@ -725,7 +732,7 @@ class TaskListItem extends React.Component {
<div className="col-sm-5 col-xs-12 name">
<i onClick={this.toggleExpanded} className={"clickable far " + (this.state.expanded ? "fa-minus-square" : " fa-plus-square")}></i> <a href="javascript:void(0);" onClick={this.toggleExpanded} className="name-link">{name}</a>
{userTags.length > 0 ?
userTags.map((t, i) => <div key={i} className="tag-badge small-badge">{t}</div>)
userTags.map((t, i) => <div key={i} className="tag-badge small-badge" onClick={this.handleTagClick(t)}>{t}</div>)
: ""}
</div>
<div className="col-sm-1 col-xs-5 details">

Wyświetl plik

@ -123,4 +123,49 @@
cursor: pointer;
}
}
.filter-dropdown{
max-width: 320px;
padding-bottom: 6px;
}
.filter-text{
height: 25px;
margin-left: 7px;
margin-right: 6px;
margin-bottom: 4px;
padding-left: 4px;
padding-right: 4px;
border-width: 1px;
border-radius: 3px;
display: block;
width: 100%;
}
.filter-text-container,.tag-selection{
display: flex;
}
.filter-checkbox{
margin-left: 8px;
}
.filter-checkbox-label{
font-weight: normal;
position: relative;
top: 4px;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 4px;
width: 100%;
}
.clear-container{
text-align: right;
margin-top: 2px;
margin-right: 6px;
}
.quick-clear-filter{
margin-right: 6px !important;
}
}