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]+")
|
tag_pattern = re.compile("#[^\s]+")
|
||||||
tags = set(re.findall(tag_pattern, value))
|
tags = set(re.findall(tag_pattern, value))
|
||||||
|
|
||||||
deep_tags = set([t for t in tags if t.startswith("##")])
|
task_tags = set([t for t in tags if t.startswith("##")])
|
||||||
project_tags = tags - deep_tags
|
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]
|
project_tags = [t.replace("#", "") for t in project_tags]
|
||||||
|
|
||||||
names = re.sub("\s+", " ", re.sub(tag_pattern, "", value)).strip()
|
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")
|
name_query = SearchQuery(names, search_type="plain")
|
||||||
qs = qs.annotate(n_search=project_name_vec + task_name_vec).filter(n_search=name_query)
|
qs = qs.annotate(n_search=project_name_vec + task_name_vec).filter(n_search=name_query)
|
||||||
|
|
||||||
if len(deep_tags) > 0:
|
if len(task_tags) > 0:
|
||||||
project_tags_vec = SearchVector("tags")
|
task_tags_vec = SearchVector("task__tags")
|
||||||
task_tags_vec = SearchVector(StringAgg("task__tags", delimiter=' '))
|
tags_query = SearchQuery(task_tags[0])
|
||||||
tags_query = SearchQuery(deep_tags[0])
|
for t in task_tags[1:]:
|
||||||
for t in deep_tags[1:]:
|
|
||||||
tags_query = tags_query & SearchQuery(t)
|
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:
|
if len(project_tags) > 0:
|
||||||
project_tags_vec = SearchVector("tags")
|
project_tags_vec = SearchVector("tags")
|
||||||
|
|
|
@ -122,6 +122,8 @@ class Paginator extends React.Component {
|
||||||
ref={(domNode) => { this.searchInput = domNode}}
|
ref={(domNode) => { this.searchInput = domNode}}
|
||||||
className="form-control search theme-border-secondary-07"
|
className="form-control search theme-border-secondary-07"
|
||||||
placeholder={_("Search names or #tags")}
|
placeholder={_("Search names or #tags")}
|
||||||
|
spellCheck="false"
|
||||||
|
autoComplete="false"
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onKeyDown={this.handleSearchKeyDown}
|
onKeyDown={this.handleSearchKeyDown}
|
||||||
onChange={this.handleSearchChange} />
|
onChange={this.handleSearchChange} />
|
||||||
|
|
|
@ -41,7 +41,9 @@ class ProjectListItem extends React.Component {
|
||||||
importing: false,
|
importing: false,
|
||||||
buttons: [],
|
buttons: [],
|
||||||
sortKey: "-created_at",
|
sortKey: "-created_at",
|
||||||
filterTags: []
|
filterTags: [],
|
||||||
|
selectedTags: [],
|
||||||
|
filterText: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sortItems = [{
|
this.sortItems = [{
|
||||||
|
@ -90,6 +92,13 @@ class ProjectListItem extends React.Component {
|
||||||
if (this.refreshRequest) this.refreshRequest.abort();
|
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(){
|
getDefaultUploadState(){
|
||||||
return {
|
return {
|
||||||
uploading: false,
|
uploading: false,
|
||||||
|
@ -500,7 +509,42 @@ class ProjectListItem extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
tagsChanged = (filterTags) => {
|
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() {
|
render() {
|
||||||
|
@ -585,17 +629,35 @@ class ProjectListItem extends React.Component {
|
||||||
|
|
||||||
{this.state.showTaskList && numTasks > 1 ?
|
{this.state.showTaskList && numTasks > 1 ?
|
||||||
<div className="task-filters">
|
<div className="task-filters">
|
||||||
{filterTags.length > 0 ?
|
<div className="btn-group">
|
||||||
<div className="btn-group">
|
{this.state.selectedTags.length || this.state.filterText !== "" ?
|
||||||
<i className='fa fa-filter'></i>
|
<a className="quick-clear-filter" href="javascript:void(0)" onClick={this.clearFilter}>×</a>
|
||||||
<a href="javascript:void(0);" className="dropdown-toggle" data-toggle-outside data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
: ""}
|
||||||
{_("Filter")}
|
<i className='fa fa-filter'></i>
|
||||||
</a>
|
<a href="javascript:void(0);" onClick={this.onOpenFilter} className="dropdown-toggle" data-toggle-outside data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<ul className="dropdown-menu dropdown-menu-right">
|
{_("Filter")}
|
||||||
{filterTags.map(t => <li key={t}>{t}</li>)}
|
</a>
|
||||||
</ul>
|
<ul className="dropdown-menu dropdown-menu-right filter-dropdown">
|
||||||
</div>
|
<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">
|
<div className="btn-group">
|
||||||
<i className='fa fa-sort-alpha-down'></i>
|
<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">
|
<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}
|
onTaskMoved={this.taskMoved}
|
||||||
hasPermission={this.hasPermission}
|
hasPermission={this.hasPermission}
|
||||||
onTagsChanged={this.tagsChanged}
|
onTagsChanged={this.tagsChanged}
|
||||||
|
onTagClicked={this.selectTag}
|
||||||
history={this.props.history}
|
history={this.props.history}
|
||||||
/> : ""}
|
/> : ""}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,8 @@ class TaskList extends React.Component {
|
||||||
onDelete: PropTypes.func,
|
onDelete: PropTypes.func,
|
||||||
onTaskMoved: PropTypes.func,
|
onTaskMoved: PropTypes.func,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
hasPermission: PropTypes.func.isRequired,
|
||||||
onTagsChanged: PropTypes.func
|
onTagsChanged: PropTypes.func,
|
||||||
|
onTagClicked: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
|
@ -21,7 +22,9 @@ class TaskList extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
tasks: [],
|
tasks: [],
|
||||||
error: "",
|
error: "",
|
||||||
loading: true
|
loading: true,
|
||||||
|
filterText: "",
|
||||||
|
filterTags: []
|
||||||
};
|
};
|
||||||
|
|
||||||
this.refresh = this.refresh.bind(this);
|
this.refresh = this.refresh.bind(this);
|
||||||
|
@ -42,6 +45,10 @@ class TaskList extends React.Component {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyFilter(text, tags){
|
||||||
|
this.setState({filterText: text, filterTags: tags});
|
||||||
|
}
|
||||||
|
|
||||||
loadTaskList(){
|
loadTaskList(){
|
||||||
this.setState({loading: true});
|
this.setState({loading: true});
|
||||||
|
|
||||||
|
@ -112,6 +119,17 @@ class TaskList extends React.Component {
|
||||||
setTimeout(() => this.notifyTagsChanged(), 0);
|
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() {
|
render() {
|
||||||
let message = "";
|
let message = "";
|
||||||
if (this.state.loading){
|
if (this.state.loading){
|
||||||
|
@ -124,7 +142,10 @@ class TaskList extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="task-list">
|
<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
|
<TaskListItem
|
||||||
data={task}
|
data={task}
|
||||||
key={task.id}
|
key={task.id}
|
||||||
|
@ -133,6 +154,7 @@ class TaskList extends React.Component {
|
||||||
onMove={this.moveTask}
|
onMove={this.moveTask}
|
||||||
onDuplicate={this.refresh}
|
onDuplicate={this.refresh}
|
||||||
onEdited={this.taskEdited}
|
onEdited={this.taskEdited}
|
||||||
|
onTagClicked={this.props.onTagClicked}
|
||||||
hasPermission={this.props.hasPermission}
|
hasPermission={this.props.hasPermission}
|
||||||
history={this.props.history} />
|
history={this.props.history} />
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -25,7 +25,8 @@ class TaskListItem extends React.Component {
|
||||||
onMove: PropTypes.func,
|
onMove: PropTypes.func,
|
||||||
onDuplicate: PropTypes.func,
|
onDuplicate: PropTypes.func,
|
||||||
hasPermission: PropTypes.func,
|
hasPermission: PropTypes.func,
|
||||||
onEdited: PropTypes.func
|
onEdited: PropTypes.func,
|
||||||
|
onTagClicked: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
|
@ -404,6 +405,12 @@ class TaskListItem extends React.Component {
|
||||||
}else return false;
|
}else return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTagClick = t => {
|
||||||
|
return () => {
|
||||||
|
if (this.props.onTagClicked) this.props.onTagClicked(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const task = this.state.task;
|
const task = this.state.task;
|
||||||
const name = task.name !== null ? task.name : interpolate(_("Task #%(number)s"), { number: task.id });
|
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">
|
<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>
|
<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.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>
|
||||||
<div className="col-sm-1 col-xs-5 details">
|
<div className="col-sm-1 col-xs-5 details">
|
||||||
|
|
|
@ -123,4 +123,49 @@
|
||||||
cursor: pointer;
|
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