Skip to content

Commit

Permalink
Add search for tags (#416)
Browse files Browse the repository at this point in the history
* Search for tags by adding prefixes is supported for "tag", "name" and "status"
* Without a prefix the properties tag, displayName and status are searched for matches
* Tag buttons support search on click
* Filter projects is executed by a web worker to avoid blocking the UI-thread
* Add debounce to searchinput to avoid re-rendering after every search input
* Highlight displayname, status and tags on search match
* Implement search via URL queries
* E2e-tests are created
* Adjust project search documentation
* Update CHANGELOG.md
  • Loading branch information
Stezido authored and daniel.arnauer committed Feb 4, 2020
1 parent 02a41d9 commit 9b67735
Show file tree
Hide file tree
Showing 25 changed files with 1,961 additions and 1,267 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

<!-- ### Added -->
### Added

- Search via URL query [#353](https://github.com/openkfw/TruBudget/issues/353)

### Changed

- The analytics total budget is shown whether the user has insufficient permissions or not [#410](https://github.com/openkfw/TruBudget/pull/410)
- Highlight matches when searching [#356](https://github.com/openkfw/TruBudget/issues/356)
- Projects can be searched via prefixes. Tag, display name and status are searched for matches. [#359](https://github.com/openkfw/TruBudget/issues/359)
- Tags can be searched via click on tag [#367](https://github.com/openkfw/TruBudget/issues/367)

<!-- ### Deprecated -->

Expand Down
16 changes: 15 additions & 1 deletion doc/wiki/User-Guide/Projects/Project.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,20 @@ View all projects where the current user has view-permissions on.

**Description:**

Filter projects based on name, description and tag in the overview page.
There are 3 ways how projects can be filtered on the overview page:

1. Searchbar
2. Tag-Button
3. URL

The Searchbar can be used to search all projects for a term included in name, tag or status. Prefixes can be used to specify the search context (e.g. tag:mycustomtag). If no prefix is used display name, tag and status is searched for a match. After typing the project list and the URL are instantly updated. The URL can then be shared to other users including the filter.
The Tag-Button can be clicked to only show projects including the clicked tag.
The URL supports query parameters which are instantly updated when typing search terms into the searchbar.

**Notes:**

- Use prefixes to search specific attributes. Available Prefixes: tag, name, status
- The filter options can be easily shared by copying the link after typing in the search terms into the searchbar.

**Instructions:**

Expand Down Expand Up @@ -218,6 +231,7 @@ The history contains all activities done directly refer to the current project.
- Tags should not contain whitespaces or any special characters except "\_", "." or "-".
- Tags can contain alphanumeric characters and can be up to 15 characters.
- Tags cannot not start or end with the special characters listed above
- Tag-Buttons in the project list of the overview page can filter projects by tag

**Instructions:**

Expand Down
107 changes: 66 additions & 41 deletions e2e-test/cypress/integration/login_spec.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
describe("Login", function() {
let routes;
let projectId, subprojectId;
const routes = {
overview: "projects",
users: "users",
notifications: "notifications",
nodes: "nodes",
projectDetails: `projects/${projectId}`,
subprojectDetails: `projects/${projectId}/${subprojectId}`,
notFound: "notfound"
};

before(function() {
cy.login();
cy.createProject("p-subp-assign", "subproject assign test").then(({ id }) => {
const projectId = id;
cy.createSubproject(projectId, "subproject assign test").then(({ id }) => {
const subprojectId = id;
cy.createProject("p-login", "login test").then(({ id }) => {
projectId = id;
cy.createSubproject(projectId, "sp-login").then(({ id }) => {
subprojectId = id;
// Logout
localStorage.setItem("state", undefined);

routes = [
"projects",
"users",
"notifications",
"nodes",
`projects/${projectId}`,
`projects/${projectId}/${subprojectId}`,
"notfound"
];
});
});
});
Expand All @@ -27,31 +26,34 @@ describe("Login", function() {
cy.visit(`/`);
});

it(`Log in and out on every route`, function() {
routes.forEach(route => {
// Login process
cy.get("#loginpage")
.should("be.visible")
.get("#username")
.should("be.visible")
.type("mstein")
.should("have.value", "mstein")
.get("#password")
.should("be.visible")
.type("test")
.should("have.value", "test")
.get("#loginbutton")
.click();
// Check if logged in correctly
cy.get("#logoutbutton").should("be.visible");
// Logout on specific route
cy.visit(`/${route}`);
cy.get("#logoutbutton")
.should("be.visible")
.click();
// Check if logged out correctly
cy.get("#loginpage").should("be.visible");
});
it(`Log in and out on overview page`, function() {
loginUi();
logout(routes.overview);
});

it(`Log in and out on users page`, function() {
loginUi();
logout(routes.users);
});
it(`Log in and out on notifications page`, function() {
loginUi();
logout(routes.notifications);
});
it(`Log in and out on nodes page`, function() {
loginUi();
logout(routes.nodes);
});
it(`Log in and out on projectDetails page`, function() {
loginUi();
logout(routes.projectDetails);
});
it(`Log in and out on subprojectDetails page`, function() {
loginUi();
logout(routes.subprojectDetails);
});
it(`Log in and out on a page that's not found`, function() {
loginUi();
logout(routes.notFound);
});

it("Reject wrong inputs", function() {
Expand All @@ -61,10 +63,33 @@ describe("Login", function() {
.type("foo")
.should("have.value", "foo");
cy.get("#password")
.should("be.visible")
.type("bar")
.should("have.value", "bar");
cy.get("#loginbutton").click();
cy.get("#password-helper-text").should("be.visible");
});
});

function logout(route) {
cy.visit(`/${route}`);
cy.get("#logoutbutton")
.should("be.visible")
.click();
// Check if logged out correctly
cy.get("#loginpage").should("be.visible");
}

function loginUi() {
cy.get("#loginpage")
.should("be.visible")
.get("#username")
.type("mstein")
.should("have.value", "mstein")
.get("#password")
.type("test")
.should("have.value", "test")
.get("#loginbutton")
.click();
// Check if logged in correctly
cy.get("#logoutbutton").should("be.visible");
}
72 changes: 0 additions & 72 deletions e2e-test/cypress/integration/navigation_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,78 +68,6 @@ describe("Navigation", function() {
});
});

it("Filter projects by display name", function() {
// Set a unique project name
const projectDisplayName = Math.floor(Math.random() * 10000000000);

// Create project which will then be displayed after filtering
cy.createProject(projectDisplayName, projectDisplayName, [])
.then(() => cy.visit(`/projects`))
.then(() => {
cy.get("[data-test=toggle-project-search]").click();
cy.get("[data-test=project-search-field]").should("be.visible");
cy.get("[data-test=project-search-field] input").type(projectDisplayName);
// Since project name is unique, there can only be one match
cy.get("[data-test*=project-card]").then(res => assert.equal(res.length, 1));

// Check the functionality of the clear button
cy.get("[data-test=clear-project-search]").click();
cy.get("[data-test=project-search-field]").should("not.be.visible");
});
});

it("Search bar is closed and reset when viewing project details", function() {
// Set a unique project name
const projectDisplayName = Math.floor(Math.random() * 10000000000);

// Create project which will then be displayed after filtering
cy.createProject(projectDisplayName, projectDisplayName, [])
.then(() => cy.visit(`/projects`))
.then(() => {
cy.get("[data-test=toggle-project-search]").click();
cy.get("[data-test=project-search-field]").should("be.visible");
cy.get("[data-test=project-search-field] input").type(projectDisplayName);
// Since project name is unique, there can only be one match
cy.get("[data-test*=project-card]").then(res => assert.equal(res.length, 1));

// Go to project
cy.get("[data-test*=project-view-button]")
.first()
.click();

cy.get("[data-test=project-search-field]").should("not.be.visible");
cy.get("[data-test=toggle-project-search]").should("be.disabled");

cy.visit("/projects");
// Search field should be empty
cy.get("[data-test=toggle-project-search]").click();
cy.get("[data-test=project-search-field] input").should("have.value", "");
});
});

it("Search bar is closed and reset when clicking on 'Main' breadcrumb", function() {
// Set a unique project name
const projectDisplayName = Math.floor(Math.random() * 10000000000);

// Create project which will then be displayed after filtering
cy.createProject(projectDisplayName, projectDisplayName, [])
.then(() => cy.visit(`/projects`))
.then(() => {
cy.get("[data-test=toggle-project-search]").click();
cy.get("[data-test=project-search-field]").should("be.visible");
cy.get("[data-test=project-search-field] input").type(projectDisplayName);
// Since project name is unique, there can only be one match
cy.get("[data-test*=project-card]").then(res => assert.equal(res.length, 1));

// Go to project
cy.get("[data-test=breadcrumb-Main]").click();

cy.get("[data-test=project-search-field]").should("not.be.visible");
cy.get("[data-test=toggle-project-search]").should("be.enabled");
// TODO: Test what happens when you click on a project
});
});

it("The notification button redirects to the notification page", function() {
cy.get("[data-test=navbar-notification-button]").should("be.visible");
cy.get("[data-test=navbar-notification-button]").click();
Expand Down
Loading

0 comments on commit 9b67735

Please sign in to comment.