1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

(feat) filter for status in project gallery

This commit is contained in:
Karen 2023-01-19 10:29:23 +01:00 committed by Sylvain
parent 4aba30c5e3
commit d80cc4769a
29 changed files with 296 additions and 76 deletions

View File

@ -68,13 +68,13 @@ class API::ProjectsController < API::ApiController
end
def project_params
params.require(:project).permit(:name, :description, :tags, :machine_ids, :component_ids, :theme_ids, :licence_id, :licence_id, :state,
params.require(:project).permit(:name, :description, :tags, :machine_ids, :component_ids, :theme_ids, :licence_id, :status_id, :state,
user_ids: [], machine_ids: [], component_ids: [], theme_ids: [],
project_image_attributes: [:attachment],
project_caos_attributes: %i[id attachment _destroy],
project_steps_attributes: [
:id, :description, :title, :_destroy, :step_nb,
project_step_images_attributes: %i[id attachment _destroy]
{ project_step_images_attributes: %i[id attachment _destroy] }
])
end
end

View File

@ -0,0 +1,25 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { Status } from '../models/status';
export default class StatusAPI {
static async index (): Promise<Array<Status>> {
const res: AxiosResponse<Array<Status>> = await apiClient.get('/api/statuses');
return res?.data;
}
static async create (newStatus: Status): Promise<Status> {
const res: AxiosResponse<Status> = await apiClient.post('/api/statuses', { status: newStatus });
return res?.data;
}
static async update (updatedStatus: Status): Promise<Status> {
const res: AxiosResponse<Status> = await apiClient.patch(`/api/statuses/${updatedStatus.id}`, { status: updatedStatus });
return res?.data;
}
static async destroy (statusId: number): Promise<void> {
const res: AxiosResponse<void> = await apiClient.delete(`/api/statuses/${statusId}`);
return res?.data;
}
}

View File

@ -1,17 +1,99 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { react2angular } from 'react2angular';
import Select from 'react-select';
import { useTranslation } from 'react-i18next';
import StatusAPI from '../../api/status';
import { IApplication } from '../../models/application';
import { SelectOption } from '../../models/select';
import { Loader } from '../base/loader';
import { Status } from '../../models/status';
declare const Application: IApplication;
/**
* To do documentation
*/
interface StatusFilterProps {
currentStatusIndex: number,
onFilterChange: (status: Status) => void,
onError: (message: string) => void
}
/**
* Implement filtering projects by their status
*/
export const StatusFilter: React.FC<StatusFilterProps> = ({ currentStatusIndex, onError, onFilterChange }) => {
const { t } = useTranslation('public');
const defaultValue = { value: null, label: t('app.public.status_filter.all_statuses') };
const [statusesList, setStatusesList] = useState([]);
const [currentOption, setCurrentOption] = useState(defaultValue);
/**
* From the statusesList (retrieved from API) and a default Value, generates an Array of options conform to react-select
*/
const buildOptions = (): Array<SelectOption<number|void>> => {
const apiStatusesList = statusesList.map(status => {
return { value: status.id, label: status.label };
});
return [defaultValue, ...apiStatusesList];
};
/**
* On component mount, asynchronously load the full list of statuses
*/
useEffect(() => {
StatusAPI.index()
.then(data => setStatusesList(data))
.catch(e => onError(e));
}, []);
// If currentStatusIndex is provided, set currentOption accordingly
useEffect(() => {
const selectedOption = statusesList.find((status) => status.id === currentStatusIndex);
setCurrentOption(selectedOption);
}, [currentStatusIndex, statusesList]);
/**
* Callback triggered when the admin selects a status in the dropdown list
*/
const handleStatusSelected = (option: SelectOption<number>): void => {
onFilterChange({ id: option.value, label: option.label });
setCurrentOption(option);
};
const selectStyles = {
control: (baseStyles, state) => ({
...baseStyles,
boxShadow: state.isFocused ? 'inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(253, 222, 63, 0.6);' : 'grey',
border: state.isFocused ? '1px solid #fdde3f' : '1px solid #c4c4c4',
color: '#555555',
'&:hover': {
borderColor: state.isFocused ? '#fdde3f' : '#c4c4c4'
}
}),
singleValue: (baseStyles) => ({
...baseStyles,
color: '#555555'
})
};
export const StatusFilter = () => {
return (
<p> Hello </p>
<div>
<Select defaultValue={currentOption}
value={currentOption}
id="status"
className="status-select"
onChange={handleStatusSelected}
options={buildOptions()}
styles={selectStyles}
aria-label={t('app.public.status_filter.select_status')}/>
</div>
);
};
Application.Components.component('statusFilter', react2angular(StatusFilter, []));
const StatusFilterWrapper: React.FC<StatusFilterProps> = (props) => {
return (
<Loader>
<StatusFilter {...props} />
</Loader>
);
};
Application.Components.component('statusFilter', react2angular(StatusFilterWrapper, ['currentStatusIndex', 'onError', 'onFilterChange']));

View File

@ -43,7 +43,7 @@
* - $state (Ui-Router) [ 'app.public.projects_show', 'app.public.projects_list' ]
*/
class ProjectsController {
constructor ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t) {
constructor ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, _t) {
// remove codeview from summernote editor
$scope.summernoteOptsProject = angular.copy($rootScope.summernoteOpts);
$scope.summernoteOptsProject.toolbar[6][1].splice(1, 1);
@ -88,6 +88,16 @@ class ProjectsController {
});
});
// Retrieve the list of statuses from the server
Status.query().$promise.then(function (data) {
$scope.statuses = data.map(function (d) {
return ({
id: d.id,
label: d.label
});
});
});
// Total number of documentation steps for the current project
$scope.totalSteps = $scope.project.project_steps_attributes.length;
@ -296,7 +306,8 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
from: ($location.$$search.from || undefined),
machine_id: (parseInt($location.$$search.machine_id) || undefined),
component_id: (parseInt($location.$$search.component_id) || undefined),
theme_id: (parseInt($location.$$search.theme_id) || undefined)
theme_id: (parseInt($location.$$search.theme_id) || undefined),
status_id: (parseInt($location.$$search.status_id) || undefined)
};
// list of projects to display
@ -311,6 +322,16 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
// list of components / used for filtering
$scope.components = componentsPromise;
$scope.onStatusChange = function (status) {
if (status) {
$scope.search.status_id = status.id;
} else {
$scope.search.status_id = undefined;
}
$scope.setUrlQueryParams($scope.search);
$scope.triggerSearch();
};
/**
* Callback triggered when the button "search from the whole network" is toggled
*/
@ -333,13 +354,17 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
* Reinitialize the search filters (used by the projects from the instance DB) and trigger a new search query
*/
$scope.resetFiltersAndTriggerSearch = function () {
$scope.search.q = '';
$scope.search.from = undefined;
$scope.search.machine_id = undefined;
$scope.search.component_id = undefined;
$scope.search.theme_id = undefined;
$scope.setUrlQueryParams($scope.search);
$scope.triggerSearch();
setTimeout(() => {
$scope.search.q = '';
$scope.search.from = undefined;
$scope.search.machine_id = undefined;
$scope.search.component_id = undefined;
$scope.search.theme_id = undefined;
$scope.search.status_id = undefined;
$scope.$apply();
$scope.setUrlQueryParams($scope.search);
$scope.triggerSearch();
}, 50);
};
/**
@ -394,6 +419,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
updateUrlParam('theme_id', search.theme_id);
updateUrlParam('component_id', search.component_id);
updateUrlParam('machine_id', search.machine_id);
updateUrlParam('status_id', search.status_id);
return true;
};
@ -470,8 +496,8 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
/**
* Controller used in the project creation page
*/
Application.Controllers.controller('NewProjectController', ['$rootScope', '$scope', '$state', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', '_t',
function ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, Diacritics, dialogs, allowedExtensions, _t) {
Application.Controllers.controller('NewProjectController', ['$rootScope', '$scope', '$state', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', 'Status', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', '_t',
function ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, CSRF, Diacritics, dialogs, allowedExtensions, _t) {
CSRF.setMetaTags();
// API URL where the form will be posted
@ -503,15 +529,15 @@ Application.Controllers.controller('NewProjectController', ['$rootScope', '$scop
};
// Using the ProjectsController
return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t);
return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, _t);
}
]);
/**
* Controller used in the project edition page
*/
Application.Controllers.controller('EditProjectController', ['$rootScope', '$scope', '$state', '$transition$', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', '_t',
function ($rootScope, $scope, $state, $transition$, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, _t) {
Application.Controllers.controller('EditProjectController', ['$rootScope', '$scope', '$state', '$transition$', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', 'Status', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', '_t',
function ($rootScope, $scope, $state, $transition$, Project, Machine, Member, Component, Theme, Licence, Status, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, _t) {
/* PUBLIC SCOPE */
// API URL where the form will be posted
@ -557,7 +583,7 @@ Application.Controllers.controller('EditProjectController', ['$rootScope', '$sco
}
// Using the ProjectsController
return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t);
return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, _t);
};
// !!! MUST BE CALLED AT THE END of the controller

View File

@ -0,0 +1,4 @@
export interface Status {
id?: number,
label: string,
}

View File

@ -289,7 +289,7 @@ angular.module('application.router', ['ui.router'])
// projects
.state('app.public.projects_list', {
url: '/projects?q&page&theme_id&component_id&machine_id&from&whole_network',
url: '/projects?q&page&theme_id&component_id&machine_id&from&whole_network&status_id',
reloadOnSearch: false,
views: {
'main@': {

View File

@ -0,0 +1,11 @@
'use strict';
Application.Services.factory('Status', ['$resource', function ($resource) {
return $resource('/api/statuses/:id',
{ id: '@id' }, {
update: {
method: 'PUT'
}
}
);
}]);

View File

@ -162,6 +162,23 @@
<div class="col-sm-12 col-md-12 col-lg-3">
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small">
<h3 translate>{{ 'app.shared.project.status' }}</h3>
</div>
<div class="widget-content no-bg wrapper">
<!-- TODO: be able to remove the selected option -->
<ui-select ng-model="project.status_id">
<ui-select-match>
<span ng-bind="$select.selected.label"></span>
<input type="hidden" name="project[status_id]" value="{{$select.selected.id}}" />
</ui-select-match>
<ui-select-choices repeat="s.id as s in (statuses | filter: $select.search)">
<span ng-bind-html="s.label | highlight: $select.search"></span>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small">

View File

@ -50,7 +50,7 @@
</div>
</form>
</div>
<div class="inline" ng-if="!openlab.searchOverWholeNetwork">
<div class="col-md-12" ng-if="!openlab.searchOverWholeNetwork">
<div class="col-md-3 m-b" ng-show="isAuthenticated()">
<select ng-model="search.from" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control">
<option value="" translate>{{ 'app.public.projects_list.all_projects' }}</option>
@ -58,29 +58,29 @@
<option value="collaboration" translate>{{ 'app.public.projects_list.projects_to_whom_i_take_part_in' }}</option>
</select>
</div>
<div class="col-md-3 m-b">
<select ng-model="search.machine_id" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control" ng-options="m.id as m.name for m in machines">
<option value="" translate>{{ 'app.public.projects_list.all_machines' }}</option>
</select>
</div>
<div class="col-md-3 m-b">
<div class="col-md-2 m-b">
<select ng-model="search.theme_id" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control" ng-options="t.id as t.name for t in themes">
<option value="" translate>{{ 'app.public.projects_list.all_themes' }}</option>
</select>
</div>
<div class="col-md-3 m-b">
<div class="col-md-2 m-b">
<select ng-model="search.component_id" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control" ng-options="t.id as t.name for t in components">
<option value="" translate>{{ 'app.public.projects_list.all_materials' }}</option>
</select>
</div>
<status-filter></status-filter>
<div class="col-md-2 m-b">
<status-filter on-filter-change="onStatusChange" current-status-index="search.status_id"></status-filter>
</div>
</div>
</div>
<div class="row">
<span class="col-md-12" ng-show="projects && (projects.length == 0)"> {{ 'app.public.projects_list.project_search_result_is_empty' | translate }} </span>
<div class="col-xs-12 col-sm-6 col-md-3" ng-repeat="project in projects" ng-click="showProject(project)">

View File

@ -106,6 +106,15 @@
</ul>
</section>
<section class="widget panel b-a m" ng-if="project.status">
<div class="panel-heading b-b">
<h3 translate>{{ 'app.public.projects_show.status' }}</h3>
</div>
<div class="panel-body">
{{ project.status.label }}
</div>
</section>
<section class="widget panel b-a m" ng-if="project.machines">
<div class="panel-heading b-b">
<span class="badge bg-warning pull-right">{{project.machines.length}}</span>

View File

@ -2,6 +2,7 @@
# Component is a material that can be used in Projects.
class Component < ApplicationRecord
has_and_belongs_to_many :projects, join_table: 'projects_components'
has_many :projects_components, dependent: :destroy
has_many :projects, through: :projects_components
validates :name, presence: true, length: { maximum: 50 }
end

View File

@ -21,16 +21,21 @@ class Project < ApplicationRecord
has_many :project_caos, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :project_caos, allow_destroy: true, reject_if: :all_blank
has_and_belongs_to_many :machines, join_table: 'projects_machines'
has_and_belongs_to_many :spaces, join_table: 'projects_spaces'
has_and_belongs_to_many :components, join_table: 'projects_components'
has_and_belongs_to_many :themes, join_table: 'projects_themes'
has_many :projects_machines, dependent: :destroy
has_many :machines, through: :projects_machines
has_many :projects_spaces, dependent: :destroy
has_many :spaces, through: :projects_spaces
has_many :projects_components, dependent: :destroy
has_many :components, through: :projects_components
has_many :projects_themes, dependent: :destroy
has_many :themes, through: :projects_themes
has_many :project_users, dependent: :destroy
has_many :users, through: :project_users
belongs_to :author, foreign_key: :author_statistic_profile_id, class_name: 'StatisticProfile'
belongs_to :licence, foreign_key: :licence_id
belongs_to :author, foreign_key: :author_statistic_profile_id, class_name: 'StatisticProfile', inverse_of: :my_projects
belongs_to :licence, inverse_of: :projects
belongs_to :status, inverse_of: :projects
has_many :project_steps, dependent: :destroy
accepts_nested_attributes_for :project_steps, allow_destroy: true
@ -54,12 +59,13 @@ class Project < ApplicationRecord
scope :published_or_drafts, lambda { |author_profile|
where("state = 'published' OR (state = 'draft' AND author_statistic_profile_id = ?)", author_profile)
}
scope :user_projects, ->(author_profile) { where('author_statistic_profile_id = ?', author_profile) }
scope :user_projects, ->(author_profile) { where(author_statistic_profile: author_profile) }
scope :collaborations, ->(collaborators_ids) { joins(:project_users).where(project_users: { user_id: collaborators_ids }) }
scope :with_machine, ->(machines_ids) { joins(:projects_machines).where(projects_machines: { machine_id: machines_ids }) }
scope :with_theme, ->(themes_ids) { joins(:projects_themes).where(projects_themes: { theme_id: themes_ids }) }
scope :with_component, ->(component_ids) { joins(:projects_components).where(projects_components: { component_id: component_ids }) }
scope :with_space, ->(spaces_ids) { joins(:projects_spaces).where(projects_spaces: { space_id: spaces_ids }) }
scope :with_status, ->(statuses_ids) { where(status_id: statuses_ids) }
pg_search_scope :search,
against: :search_vector,
using: {

View File

@ -1,7 +0,0 @@
# frozen_string_literal: true
# Here we link status to their projects
class ProjectStatus < ApplicationRecord
belongs_to :project
belongs_to :status
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
# ProjectsComponent is the relation table between a Component and a Project.
class ProjectsComponent < ApplicationRecord
belongs_to :component
belongs_to :project
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
# ProjectsTheme is the relation table between a Theme and a Project.
class ProjectsTheme < ApplicationRecord
belongs_to :theme
belongs_to :project
end

View File

@ -3,4 +3,5 @@
# Set statuses for projects (new, pending, done...)
class Status < ApplicationRecord
validates :label, presence: true
has_many :projects, dependent: :nullify
end

View File

@ -2,6 +2,7 @@
# Theme is an optional filter used to categorize Projects
class Theme < ApplicationRecord
has_and_belongs_to_many :projects, join_table: :projects_themes
has_many :projects_themes, dependent: :destroy
has_many :projects, through: :projects_themes
validates :name, presence: true, length: { maximum: 80 }
end
end

View File

@ -2,7 +2,6 @@
# Provides methods for Project
class ProjectService
def search(params, current_user)
connection = ActiveRecord::Base.connection
return { error: 'invalid adapter' } unless connection.instance_values['config'][:adapter] == 'postgresql'
@ -23,6 +22,7 @@ class ProjectService
records = records.with_component(query_params['component_id']) if query_params['component_id'].present?
records = records.with_theme(query_params['theme_id']) if query_params['theme_id'].present?
records = records.with_space(query_params['space_id']) if query_params['space_id'].present?
records = records.with_status(query_params['status_id']) if query_params['status_id'].present?
records = if query_params['q'].present?
records.search(query_params['q'])
else
@ -31,4 +31,4 @@ class ProjectService
{ total: records.count, projects: records.includes(:users, :project_image).page(params[:page]) }
end
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
json.projects @projects do |project|
json.extract! project, :id, :name, :licence_id, :slug, :state
json.extract! project, :id, :name, :licence_id, :slug, :state, :status
json.description sanitize(project.description)
json.author_id project.author.user_id

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
json.extract! @project, :id, :name, :tags, :created_at, :updated_at, :licence_id, :slug
json.extract! @project, :id, :name, :tags, :created_at, :updated_at, :licence_id, :status_id, :slug
json.description sanitize(@project.description)
json.author_id @project.author.user_id
json.project_image @project.project_image.attachment.large.url if @project.project_image
@ -73,3 +73,4 @@ if @project.licence.present?
json.name @project.licence.name
end
end
json.status @project.status

View File

@ -184,6 +184,9 @@ en:
all_materials: "All materials"
load_next_projects: "Load next projects"
rough_draft: "Rough draft"
status_filter:
all_statuses: "All statuses"
select_status: "Select a status"
#details of a projet
projects_show:
rough_draft: "Draft"
@ -213,6 +216,7 @@ en:
message_is_required: "Message is required."
report: "Report"
do_you_really_want_to_delete_this_project: "Do you really want to delete this project?"
status: "Status"
#list of machines
machines_list:
the_fablab_s_machines: "The machines"

View File

@ -152,6 +152,7 @@ en:
themes: "Themes"
tags: "Tags"
save_as_draft: "Save as draft"
status: "Status"
#button to book a machine reservation
reserve_button:
book_this_machine: "Book this machine"

View File

@ -8,5 +8,6 @@ class CreateStatuses < ActiveRecord::Migration[5.2]
t.timestamps
end
add_reference :projects, :status, index: true, foreign_key: true
end
end

View File

@ -1,13 +0,0 @@
# frozen_string_literal: true
# From this migration, we link status to their projects
class CreateProjectStatuses < ActiveRecord::Migration[5.2]
def change
create_table :project_statuses do |t|
t.references :project, foreign_key: true
t.references :status, foreign_key: true
t.timestamps
end
end
end

View File

@ -865,15 +865,6 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
t.index ["user_id"], name: "index_profiles_on_user_id"
end
create_table "project_statuses", force: :cascade do |t|
t.bigint "project_id"
t.bigint "status_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["project_id"], name: "index_project_statuses_on_project_id"
t.index ["status_id"], name: "index_project_statuses_on_status_id"
end
create_table "project_steps", id: :serial, force: :cascade do |t|
t.text "description"
t.integer "project_id"
@ -1409,8 +1400,6 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
add_foreign_key "prices", "plans"
add_foreign_key "product_stock_movements", "products"
add_foreign_key "products", "product_categories"
add_foreign_key "project_statuses", "projects"
add_foreign_key "project_statuses", "statuses"
add_foreign_key "project_steps", "projects"
add_foreign_key "project_users", "projects"
add_foreign_key "project_users", "users"

View File

@ -101,6 +101,15 @@ if Theme.count.zero?
])
end
if Status.count.zero?
Status.create!([
{ label: 'Nouveau' },
{ label: 'En cours' },
{ label: 'Terminé' },
{ label: 'Arrêté' }
])
end
if Training.count.zero?
Training.create!([
{

View File

@ -0,0 +1,8 @@
import { Status } from '../../../app/frontend/src/javascript/models/status';
const statuses: Array<Status> = [
{ id: 1, label: 'Mocked Status 1' },
{ id: 2, label: 'Mocked Status 2' }
];
export default statuses;

View File

@ -12,6 +12,7 @@ import machines from '../__fixtures__/machines';
import providers from '../__fixtures__/auth_providers';
import profileCustomFields from '../__fixtures__/profile_custom_fields';
import spaces from '../__fixtures__/spaces';
import statuses from '../__fixtures__/statuses';
export const server = setupServer(
rest.get('/api/groups', (req, res, ctx) => {
@ -96,6 +97,9 @@ export const server = setupServer(
}),
rest.get('/api/spaces', (req, res, ctx) => {
return res(ctx.json(spaces));
}),
rest.get('/api/statuses', (req, res, ctx) => {
return res(ctx.json(statuses));
})
);

View File

@ -0,0 +1,26 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { StatusFilter } from '../../../../app/frontend/src/javascript/components/projects/status-filter';
describe('Status Filter', () => {
test('should call onChange with option when selecting and shifting option', async () => {
const onError = jest.fn();
const onFilterChange = jest.fn();
render(<StatusFilter onError={onError} onFilterChange={onFilterChange}/>);
expect(onFilterChange).toHaveBeenCalledTimes(0);
fireEvent.keyDown(screen.getByLabelText(/app.public.status_filter.all_statuses/), { key: 'ArrowDown' });
await waitFor(() => screen.getByText('Mocked Status 1'));
fireEvent.click(screen.getByText('Mocked Status 1'));
expect(onFilterChange).toHaveBeenCalledWith({ label: 'Mocked Status 1', value: 1 });
fireEvent.keyDown(screen.getByLabelText(/app.public.status_filter.all_statuses/), { key: 'ArrowDown' });
await waitFor(() => screen.getByText('Mocked Status 2'));
fireEvent.click(screen.getByText('Mocked Status 2'));
expect(onFilterChange).toHaveBeenCalledTimes(2);
expect(onFilterChange).toHaveBeenCalledWith({ label: 'Mocked Status 2', value: 2 });
});
});