kopia lustrzana https://github.com/OpenDroneMap/WebODM
Task level filtering working
rodzic
c90b575850
commit
8c16f9a26d
|
@ -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")
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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}
|
||||
/> : ""}
|
||||
|
||||
|
|
|
@ -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} />
|
||||
))}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue