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:
parent
4aba30c5e3
commit
d80cc4769a
@ -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
|
||||
|
25
app/frontend/src/javascript/api/status.ts
Normal file
25
app/frontend/src/javascript/api/status.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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']));
|
||||
|
@ -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
|
||||
|
4
app/frontend/src/javascript/models/status.ts
Normal file
4
app/frontend/src/javascript/models/status.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface Status {
|
||||
id?: number,
|
||||
label: string,
|
||||
}
|
@ -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@': {
|
||||
|
11
app/frontend/src/javascript/services/status.js
Normal file
11
app/frontend/src/javascript/services/status.js
Normal file
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
Application.Services.factory('Status', ['$resource', function ($resource) {
|
||||
return $resource('/api/statuses/:id',
|
||||
{ id: '@id' }, {
|
||||
update: {
|
||||
method: 'PUT'
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
@ -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">
|
||||
|
@ -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)">
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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: {
|
||||
|
@ -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
|
7
app/models/projects_component.rb
Normal file
7
app/models/projects_component.rb
Normal 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
|
7
app/models/projects_theme.rb
Normal file
7
app/models/projects_theme.rb
Normal 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
|
@ -3,4 +3,5 @@
|
||||
# Set statuses for projects (new, pending, done...)
|
||||
class Status < ApplicationRecord
|
||||
validates :label, presence: true
|
||||
has_many :projects, dependent: :nullify
|
||||
end
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -8,5 +8,6 @@ class CreateStatuses < ActiveRecord::Migration[5.2]
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_reference :projects, :status, index: true, foreign_key: true
|
||||
end
|
||||
end
|
||||
|
@ -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
|
11
db/schema.rb
11
db/schema.rb
@ -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"
|
||||
|
@ -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!([
|
||||
{
|
||||
|
8
test/frontend/__fixtures__/statuses.ts
Normal file
8
test/frontend/__fixtures__/statuses.ts
Normal 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;
|
@ -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));
|
||||
})
|
||||
);
|
||||
|
||||
|
26
test/frontend/components/projects/status-filter.test.tsx
Normal file
26
test/frontend/components/projects/status-filter.test.tsx
Normal 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 });
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user