Add simple JQL implementation

This commit is contained in:
Adrian Woźniak 2021-04-29 14:47:38 +02:00
parent f824694a05
commit 98f9204fbd
7 changed files with 452 additions and 87 deletions

View File

@ -166,6 +166,10 @@
}
}
}
> .description {
padding: 5px;
}
}
> .issuesList {

View File

@ -3,7 +3,7 @@ use std::str::FromStr;
use seed::prelude::*;
use seed::*;
use crate::Msg;
use crate::{resolve_page, Msg};
#[derive(Debug, Default)]
pub struct StyledLink<'l> {
@ -24,6 +24,9 @@ impl<'l> StyledLink<'l> {
ev.stop_propagation();
if let Ok(url) = seed::Url::from_str(href.as_str()) {
url.go_and_push();
if let Some(page) = resolve_page(url) {
return Some(Msg::ChangePage(page));
}
}
}

View File

@ -15,6 +15,7 @@ use crate::components::styled_tooltip;
use crate::components::styled_tooltip::{TooltipVariant as StyledTooltip, TooltipVariant};
use crate::modals::DebugMsg;
use crate::model::{ModalType, Model, Page};
use crate::pages::issues_and_filters::IssuesAndFiltersMsg;
use crate::shared::{go_to_board, go_to_login};
use crate::ws::{flush_queue, open_socket, read_incoming, send_ws_msg};
@ -102,6 +103,9 @@ pub enum Msg {
ProjectToggleRecentlyUpdated,
ProjectClearFilters,
// issues and filters
IssuesAndFilters(IssuesAndFiltersMsg),
// inputs
StrInputChanged(FieldId, String),

View File

@ -4,17 +4,23 @@ use seed::app::Orders;
use crate::components::styled_select::StyledSelectState;
use crate::{model, FieldId, IssuesAndFiltersId, Msg};
#[derive(Debug)]
pub enum IssuesAndFiltersMsg {
RemoveFilter(usize),
}
#[derive(Debug)]
pub struct IssuesAndFiltersPage {
pub visible_issues: Vec<IssueId>,
pub active_id: Option<IssueId>,
pub current_jql_part: StyledSelectState,
pub jql_parts: Vec<JqlPart>,
pub jql: Jql,
}
impl IssuesAndFiltersPage {
pub fn new(model: &model::Model) -> Self {
let visible_issues = Self::visible_issues(model.issues());
let jql = Jql::default();
let visible_issues = Self::visible_issues(model.issues(), &jql);
let active_id = model.issues().first().as_ref().map(|issue| issue.id);
Self {
@ -24,12 +30,16 @@ impl IssuesAndFiltersPage {
FieldId::IssuesAndFilters(IssuesAndFiltersId::Jql),
vec![],
),
jql_parts: vec![],
jql,
}
}
pub fn visible_issues(issues: &[Issue]) -> Vec<IssueId> {
issues.iter().map(|issue| issue.id).collect()
pub fn visible_issues(issues: &[Issue], jql: &Jql) -> Vec<IssueId> {
issues
.iter()
.filter(|issue| jql.is_visible(issue))
.map(|issue| issue.id)
.collect()
}
pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
@ -37,11 +47,181 @@ impl IssuesAndFiltersPage {
}
}
#[derive(Debug, Default)]
pub struct Jql {
pub parts: Vec<JqlPart>,
}
impl Jql {
pub fn current_token(&self) -> Option<JqlPartType> {
if self.parts.is_empty() {
return None;
}
// [field, op, field, keyword]
match self.len() % 4 {
0 => Some(JqlPartType::Keyword),
3 => Some(JqlPartType::Value),
2 => Some(JqlPartType::Op),
1 => Some(JqlPartType::Field),
_ => None,
}
}
pub fn op(&self) -> Option<&JqlPart> {
if self.parts.is_empty() {
return None;
}
match self.len() % 4 {
// [field, op, value, keyword]
0 => self.parts.get(self.len() - 3),
// [field, op, value]
3 => self.parts.get(self.len() - 2),
// [field, op]
2 => self.parts.last(),
// [field]
_ => None,
}
}
pub fn value(&self) -> Option<&JqlPart> {
if self.parts.is_empty() {
return None;
}
match self.len() % 4 {
// [field, op, value, keyword]
0 => self.parts.get(self.len() - 2),
// [field, op, value]
3 => self.parts.last(),
_ => None,
}
}
pub fn field(&self) -> Option<&JqlPart> {
if self.parts.is_empty() {
return None;
}
match self.len() % 4 {
// [field, op, value, keyword]
0 => self.parts.get(self.len() - 3),
// [field]
1 => self.parts.last(),
// [field, op]
2 => self.parts.get(self.len() - 2),
_ => None,
}
}
pub fn keyword(&self) -> Option<&JqlPart> {
if self.parts.is_empty() {
return None;
}
match self.len() % 4 {
// [field]
0 => self.parts.last(),
_ => None,
}
}
pub fn len(&self) -> usize {
self.parts.len()
}
pub fn push(&mut self, part: JqlPart) {
self.parts.push(part);
}
pub fn remove_from(&mut self, idx: usize) {
if self.parts.is_empty() {
return;
}
if self.len() <= 4 {
self.parts.clear();
} else {
self.parts.drain((idx - 1 - (idx % 4))..);
}
}
pub fn is_visible(&self, issue: &jirs_data::Issue) -> bool {
if self.len() < 3 {
return true;
}
let mut q = (&self.parts).iter();
while let Some(field) = q.next() {
let op = match q.next() {
None => break,
Some(op) => op,
};
let value = match q.next() {
None => break,
Some(value) => value,
};
let _keyword = q.next(); // skip keyword
match (field, op, value) {
//
(
JqlPart::Field(FieldOption::Assignee),
JqlPart::Op(OpOption::Is | OpOption::Eq),
JqlPart::Value(JqlValueOption::User(id, _)),
) if !issue.user_ids.contains(id) => return false,
(
JqlPart::Field(FieldOption::Assignee),
JqlPart::Op(OpOption::IsNot | OpOption::NotEq),
JqlPart::Value(JqlValueOption::User(id, _)),
) if issue.user_ids.contains(id) => return false,
//
(
JqlPart::Field(FieldOption::Type),
JqlPart::Op(OpOption::Is | OpOption::Eq),
JqlPart::Value(JqlValueOption::Type(t)),
) if issue.issue_type != *t => {
return false;
}
(
JqlPart::Field(FieldOption::Type),
JqlPart::Op(OpOption::IsNot | OpOption::NotEq),
JqlPart::Value(JqlValueOption::Type(t)),
) if issue.issue_type == *t => {
return false;
}
//
(
JqlPart::Field(FieldOption::Priority),
JqlPart::Op(OpOption::Is | OpOption::Eq),
JqlPart::Value(JqlValueOption::Priority(p)),
) if issue.priority != *p => {
return false;
}
(
JqlPart::Field(FieldOption::Priority),
JqlPart::Op(OpOption::IsNot | OpOption::NotEq),
JqlPart::Value(JqlValueOption::Priority(p)),
) if issue.priority == *p => {
return false;
}
_ => {}
};
}
true
}
}
#[derive(Debug)]
pub enum JqlPartType {
Field,
Op,
Value,
Keyword,
}
#[derive(Debug)]
pub enum JqlPart {
Field(FieldOption),
Op(OpOption),
Value(JqlValueOption),
Keyword(KeywordOption),
}
impl JqlPart {
@ -50,6 +230,7 @@ impl JqlPart {
JqlPart::Field(f) => f.to_label(),
JqlPart::Op(op) => op.to_label(),
JqlPart::Value(v) => v.to_label(),
JqlPart::Keyword(k) => k.to_label(),
}
}
}
@ -58,6 +239,8 @@ impl JqlPart {
pub enum FieldOption {
None,
Assignee,
Type,
Priority,
}
impl FieldOption {
@ -65,6 +248,8 @@ impl FieldOption {
match self {
FieldOption::None => " ",
FieldOption::Assignee => "Assignee",
FieldOption::Type => "Type",
FieldOption::Priority => "Priority",
}
}
@ -72,6 +257,8 @@ impl FieldOption {
match self {
FieldOption::None => "none",
FieldOption::Assignee => "assignee",
FieldOption::Type => "ticketType",
FieldOption::Priority => "ticketPriority",
}
}
@ -79,6 +266,8 @@ impl FieldOption {
match self {
FieldOption::None => 0,
FieldOption::Assignee => 1,
FieldOption::Type => 2,
FieldOption::Priority => 3,
}
}
}
@ -87,6 +276,8 @@ impl From<u32> for FieldOption {
fn from(n: u32) -> Self {
match n {
1 => FieldOption::Assignee,
2 => FieldOption::Type,
3 => FieldOption::Priority,
_ => FieldOption::None,
}
}
@ -97,6 +288,8 @@ pub enum OpOption {
None,
Eq,
NotEq,
Is,
IsNot,
}
impl OpOption {
@ -105,6 +298,8 @@ impl OpOption {
OpOption::None => " ",
OpOption::Eq => "=",
OpOption::NotEq => "!=",
OpOption::Is => "IS",
OpOption::IsNot => "IS NOT",
}
}
@ -113,6 +308,8 @@ impl OpOption {
OpOption::None => "none",
OpOption::Eq => "equal",
OpOption::NotEq => "notEqual",
OpOption::Is => "is",
OpOption::IsNot => "isNot",
}
}
@ -121,6 +318,8 @@ impl OpOption {
OpOption::None => 0,
OpOption::Eq => 1,
OpOption::NotEq => 2,
OpOption::Is => 3,
OpOption::IsNot => 4,
}
}
}
@ -130,6 +329,8 @@ impl From<u32> for OpOption {
match n {
1 => OpOption::Eq,
2 => OpOption::NotEq,
3 => OpOption::Is,
4 => OpOption::IsNot,
_ => OpOption::None,
}
}
@ -138,24 +339,70 @@ impl From<u32> for OpOption {
#[derive(Debug)]
pub enum JqlValueOption {
User(UserId, UsernameString),
Priority(jirs_data::IssuePriority),
Type(jirs_data::IssueType),
}
impl JqlValueOption {
pub fn to_label(&self) -> &str {
match self {
JqlValueOption::User(_id, name) => name.as_str(),
JqlValueOption::Priority(p) => p.to_label(),
JqlValueOption::Type(t) => t.to_label(),
}
}
pub fn to_name(&self) -> &'static str {
match self {
JqlValueOption::User(_, _) => "user",
JqlValueOption::Priority(_) => "priority",
JqlValueOption::Type(_) => "type",
}
}
pub fn to_value(&self) -> u32 {
match self {
JqlValueOption::User(id, _) => (*id) as u32,
JqlValueOption::Priority(p) => (*p).into(),
JqlValueOption::Type(t) => (*t).into(),
}
}
}
#[derive(Debug)]
pub enum KeywordOption {
None,
And,
}
impl From<u32> for KeywordOption {
fn from(n: u32) -> Self {
match n {
1 => KeywordOption::And,
_ => KeywordOption::None,
}
}
}
impl KeywordOption {
pub fn to_label(&self) -> &'static str {
match self {
KeywordOption::None => " ",
KeywordOption::And => "AND",
}
}
pub fn to_name(&self) -> &'static str {
match self {
KeywordOption::None => "none",
KeywordOption::And => "and",
}
}
pub fn to_value(&self) -> u32 {
match self {
KeywordOption::None => 0,
KeywordOption::And => 1,
}
}
}

View File

@ -1,12 +1,13 @@
use seed::prelude::*;
use super::IssuesAndFiltersMsg;
use crate::components::styled_select::StyledSelectChanged;
use crate::model::{Model, PageContent};
use crate::pages::issues_and_filters::{FieldOption, JqlPart, JqlValueOption, OpOption};
use crate::pages::issues_and_filters::{
FieldOption, JqlPart, JqlPartType, JqlValueOption, KeywordOption, OpOption,
};
use crate::ws::board_load;
use crate::{FieldId, IssuesAndFiltersId, Msg, OperationKind, Page, ResourceKind};
mod jql;
use crate::{match_page, FieldId, IssuesAndFiltersId, Msg, OperationKind, Page, ResourceKind};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
if model.user.is_none() {
@ -31,8 +32,14 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
crate::match_page_mut!(model, IssuesAndFilters).update(&msg, orders);
match msg {
Msg::IssuesAndFilters(IssuesAndFiltersMsg::RemoveFilter(idx)) => {
crate::match_page_mut!(model, IssuesAndFilters)
.jql
.remove_from(idx);
}
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::ListLoaded, _) => {
let issues = super::IssuesAndFiltersPage::visible_issues(model.issues());
let jql = &match_page!(model, IssuesAndFilters).jql;
let issues = super::IssuesAndFiltersPage::visible_issues(model.issues(), jql);
let first_id = model.issues().first().as_ref().map(|issue| issue.id);
let page = crate::match_page_mut!(model, IssuesAndFilters);
if page.active_id.is_none() {
@ -52,26 +59,56 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
StyledSelectChanged::Changed(Some(n)),
) if n != 0 => {
let page = crate::match_page_mut!(model, IssuesAndFilters);
match page.jql_parts.len() % 3 {
0 => {
match page.jql.current_token() {
None | Some(JqlPartType::Keyword) => {
let field = FieldOption::from(n);
page.jql_parts.push(JqlPart::Field(field));
page.jql.push(JqlPart::Field(field));
}
1 => {
Some(JqlPartType::Field) => {
let field = OpOption::from(n);
page.jql_parts.push(JqlPart::Op(field));
page.jql.push(JqlPart::Op(field));
}
2 => {
Some(JqlPartType::Op)
if matches!(
page.jql.field(),
Some(JqlPart::Field(FieldOption::Assignee))
) =>
{
let u = match model.users.get(n as usize) {
Some(u) => u,
_ => return,
};
let field = JqlValueOption::User(u.id, u.name.clone());
page.jql_parts.push(JqlPart::Value(field));
page.jql.push(JqlPart::Value(field));
}
Some(JqlPartType::Op)
if matches!(page.jql.field(), Some(JqlPart::Field(FieldOption::Type))) =>
{
page.jql
.push(JqlPart::Value(JqlValueOption::Type(n.into())));
}
Some(JqlPartType::Op)
if matches!(
page.jql.field(),
Some(JqlPart::Field(FieldOption::Priority))
) =>
{
page.jql
.push(JqlPart::Value(JqlValueOption::Priority(n.into())));
}
Some(JqlPartType::Value) => {
let field = KeywordOption::from(n);
page.jql.push(JqlPart::Keyword(field));
}
_ => {}
}
page.current_jql_part.reset();
page.current_jql_part.opened = true;
let issues = super::IssuesAndFiltersPage::visible_issues(
model.issues(),
&crate::match_page!(model, IssuesAndFilters).jql,
);
crate::match_page_mut!(model, IssuesAndFilters).visible_issues = issues;
}
_ => {}
}

View File

@ -1,31 +0,0 @@
pub enum OpType {
Eq,
}
impl OpType {
pub fn to_str(&self) -> &str {
match self {
OpType::Eq => "=",
}
}
}
pub enum FieldType {
Assignee,
}
impl FieldType {
pub fn to_str(&self) -> &'static str {
match self {
FieldType::Assignee => "Assignee",
}
}
}
pub struct JqlNode {
op: OpType,
}
pub fn parse(_query: &[&str]) -> JqlNode {
JqlNode { op: OpType::Eq }
}

View File

@ -1,12 +1,15 @@
use seed::prelude::*;
use seed::*;
use super::super::IssuesAndFiltersMsg;
use crate::components::styled_button::ButtonVariant;
use crate::components::styled_icon::{Icon, StyledIcon};
use crate::components::styled_select::{SelectVariant, StyledSelect};
use crate::components::styled_select_child::StyledSelectOption;
use crate::model::Model;
use crate::pages::issues_and_filters::{FieldOption, IssuesAndFiltersPage, JqlPart};
use crate::pages::issues_and_filters::{
FieldOption, IssuesAndFiltersPage, Jql, JqlPart, JqlPartType, KeywordOption, OpOption,
};
use crate::styled_button::StyledButton;
use crate::Msg;
@ -28,10 +31,9 @@ fn pseudo_input(model: &Model, page: &IssuesAndFiltersPage) -> Node<Msg> {
.current_jql_part
.values
.iter()
.enumerate()
.map(|(idx, n)| field_select_option(model, idx, *n, &page.jql_parts))
.map(|n| field_select_option(model, *n, &page.jql))
.collect(),
options: Some(options(model, &page.jql_parts).into_iter()),
options: Some(options(model, &page.jql).into_iter()),
is_multi: false,
opened: page.current_jql_part.opened,
}
@ -39,60 +41,108 @@ fn pseudo_input(model: &Model, page: &IssuesAndFiltersPage) -> Node<Msg> {
div![
C!["pseudoInput"],
StyledIcon::from(Icon::Search).render(),
page.jql_parts
page.jql
.parts
.iter()
.map(|s| s.to_label())
.map(removable_part)
.enumerate()
.map(|(idx, m)| removable_part(idx, m))
.collect::<Vec<Node<Msg>>>(),
input
]
}
fn options<'l, 'm: 'l>(model: &'m Model, parts: &[JqlPart]) -> Vec<StyledSelectOption<'l>> {
log::info!("{} {:?}", parts.len(), parts);
if parts.is_empty() {
vec![field_select_option(model, 0, 1, &parts)]
} else if parts.len() % 3 == 2
&& matches!(
parts.get(parts.len() - 2),
Some(JqlPart::Field(FieldOption::Assignee))
)
{
model
.users
.iter()
fn options<'l, 'm: 'l>(model: &'m Model, jql: &Jql) -> Vec<StyledSelectOption<'l>> {
let ty = jql.current_token();
match ty {
Some(JqlPartType::Value) => {
vec![build_keyword_select_option(1)]
}
Some(JqlPartType::Field) => {
vec![
field_select_option(model, 1, jql),
field_select_option(model, 2, jql),
field_select_option(model, 3, jql),
field_select_option(model, 4, jql),
]
}
Some(JqlPartType::Op)
if matches!(jql.field(), Some(JqlPart::Field(FieldOption::Assignee))) =>
{
model
.users
.iter()
.map(|u| StyledSelectOption {
name: Some("user"),
icon: None,
text: Some(u.name.as_str()),
value: u.id as u32,
class_list: "",
variant: SelectVariant::Empty,
})
.collect()
}
Some(JqlPartType::Op)
if matches!(jql.field(), Some(JqlPart::Field(FieldOption::Priority))) =>
{
vec![
jirs_data::IssuePriority::Lowest,
jirs_data::IssuePriority::Low,
jirs_data::IssuePriority::Medium,
jirs_data::IssuePriority::High,
jirs_data::IssuePriority::Highest,
]
.into_iter()
.map(|u| StyledSelectOption {
name: Some("user"),
name: Some("priority"),
icon: None,
text: Some(u.name.as_str()),
value: u.id as u32,
text: Some(u.to_label()),
value: u.into(),
class_list: "",
variant: SelectVariant::Empty,
})
.collect()
} else {
vec![field_select_option(model, 0, 1, &parts)]
}
Some(JqlPartType::Op) if matches!(jql.field(), Some(JqlPart::Field(FieldOption::Type))) => {
vec![
jirs_data::IssueType::Task,
jirs_data::IssueType::Bug,
jirs_data::IssueType::Story,
]
.into_iter()
.map(|u| StyledSelectOption {
name: Some("type"),
icon: None,
text: Some(u.to_label()),
value: u.into(),
class_list: "",
variant: SelectVariant::Empty,
})
.collect()
}
Some(JqlPartType::Op) => {
vec![field_select_option(model, 1, jql)]
}
None | Some(JqlPartType::Keyword) => vec![
field_select_option(model, 1, jql),
field_select_option(model, 2, jql),
field_select_option(model, 3, jql),
],
}
}
fn field_select_option<'l, 'm: 'l>(
model: &'m Model,
idx: usize,
option_value: u32,
parts: &[JqlPart],
jql: &Jql,
) -> StyledSelectOption<'l> {
if parts.is_empty() {
let ty = jql.current_token();
if ty.is_none() {
return build_field_select_option(option_value);
}
match (parts.len(), parts.len() % 3) {
(_, 1) => {
return build_field_select_option(option_value);
}
(_, 2)
if matches!(
parts.get(idx - 1),
Some(JqlPart::Field(FieldOption::Assignee))
) =>
match jql.current_token() {
Some(JqlPartType::Op)
if matches!(jql.field(), Some(JqlPart::Field(FieldOption::Assignee))) =>
{
let user = model
.users_by_id
@ -108,10 +158,59 @@ fn field_select_option<'l, 'm: 'l>(
variant: SelectVariant::Empty,
}
}
Some(JqlPartType::Op)
if matches!(jql.field(), Some(JqlPart::Field(FieldOption::Priority))) =>
{
let p: jirs_data::IssuePriority = option_value.into();
StyledSelectOption {
name: Some("priority"),
icon: None,
text: Some(p.to_label()),
value: p.into(),
class_list: "",
variant: SelectVariant::Empty,
}
}
Some(JqlPartType::Op) if matches!(jql.field(), Some(JqlPart::Field(FieldOption::Type))) => {
let p: jirs_data::IssueType = option_value.into();
StyledSelectOption {
name: Some("type"),
icon: None,
text: Some(p.to_label()),
value: p.into(),
class_list: "",
variant: SelectVariant::Empty,
}
}
Some(JqlPartType::Field) => build_op_select_option(option_value),
_ => build_field_select_option(option_value),
}
}
fn build_op_select_option<'l>(option_value: u32) -> StyledSelectOption<'l> {
let v = OpOption::from(option_value);
StyledSelectOption {
name: Some(v.to_name()),
icon: None,
text: Some(v.to_label()),
value: v.to_value(),
class_list: "",
variant: SelectVariant::Empty,
}
}
fn build_keyword_select_option<'l>(option_value: u32) -> StyledSelectOption<'l> {
let v = KeywordOption::from(option_value);
StyledSelectOption {
name: Some(v.to_name()),
icon: None,
text: Some(v.to_label()),
value: v.to_value(),
class_list: "",
variant: SelectVariant::Empty,
}
}
fn build_field_select_option<'l>(option_value: u32) -> StyledSelectOption<'l> {
let v = FieldOption::from(option_value);
StyledSelectOption {
@ -124,11 +223,13 @@ fn build_field_select_option<'l>(option_value: u32) -> StyledSelectOption<'l> {
}
}
fn removable_part(part: &str) -> Node<Msg> {
fn removable_part(idx: usize, part: &str) -> Node<Msg> {
let remove = StyledButton {
variant: ButtonVariant::Empty,
icon: Some(StyledIcon::from(Icon::Trash).render()),
on_click: None,
on_click: Some(mouse_ev("click", move |_| {
Msg::IssuesAndFilters(IssuesAndFiltersMsg::RemoveFilter(idx))
})),
..Default::default()
}
.render();