mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-01 10:44:21 +08:00
Merge branch 'decoupling-settings-page' of https://github.com/biosistemika/scinote-web into zd_SCI_1566
This commit is contained in:
commit
c3562e08e9
15 changed files with 440 additions and 15 deletions
|
@ -0,0 +1,81 @@
|
|||
import React, { Component } from "react";
|
||||
import styled from "styled-components";
|
||||
import { string, number, func } from "prop-types";
|
||||
import { Grid, Row, Col } from "react-bootstrap";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
class Alert extends Component {
|
||||
static alertClass(type) {
|
||||
const classes = {
|
||||
error: "alert-danger",
|
||||
alert: "alert-warning",
|
||||
notice: "alert-info",
|
||||
success: "alert-success"
|
||||
};
|
||||
return classes[type] || classes.success;
|
||||
}
|
||||
|
||||
static glyphiconClass(type) {
|
||||
const classes = {
|
||||
error: "glyphicon-exclamation-sign",
|
||||
alert: "glyphicon-exclamation-sign",
|
||||
notice: "glyphicon-info-sign",
|
||||
success: "glyphicon-ok-sign"
|
||||
};
|
||||
return classes[type] || classes.success;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.timer = setTimeout(
|
||||
this.props.onClose,
|
||||
this.props.timeout
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
render() {
|
||||
const alertClassName =
|
||||
`alert
|
||||
${Alert.alertClass(this.props.type)}
|
||||
alert-dismissable
|
||||
alert-floating`;
|
||||
const glyphiconClassName =
|
||||
`glyphicon
|
||||
${Alert.glyphiconClass(this.props.type)}`;
|
||||
|
||||
return (
|
||||
<Wrapper className={alertClassName}>
|
||||
<Grid>
|
||||
<Row>
|
||||
<Col>
|
||||
<button type="button"
|
||||
className="close"
|
||||
data-dismiss="alert"
|
||||
aria-label="Close"
|
||||
onClick={this.props.onClose}>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<span className={glyphiconClassName} />
|
||||
<span> {this.props.message}</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</Grid>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Alert.propTypes = {
|
||||
message: string.isRequired,
|
||||
type: string.isRequired,
|
||||
timeout: number.isRequired,
|
||||
onClose: func.isRequired
|
||||
};
|
||||
|
||||
export default Alert;
|
65
app/javascript/src/components/AlertsContainer/index.jsx
Normal file
65
app/javascript/src/components/AlertsContainer/index.jsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import TransitionGroup from 'react-transition-group/TransitionGroup';
|
||||
import CSSTransition from 'react-transition-group/CSSTransition';
|
||||
import { shape, arrayOf, string, number, func } from "prop-types";
|
||||
import { clearAlert } from "../actions/AlertsActions";
|
||||
import Alert from "./components/Alert";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
class AlertsContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.renderAlert = this.renderAlert.bind(this);
|
||||
}
|
||||
|
||||
renderAlert(alert) {
|
||||
return (
|
||||
<Alert message={alert.message}
|
||||
type={alert.type}
|
||||
timeout={alert.timeout}
|
||||
onClose={() => this.props.clearAlert(alert.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Wrapper>
|
||||
<TransitionGroup>
|
||||
{this.props.alerts.map((alert) =>
|
||||
<CSSTransition key={alert.id}
|
||||
timeout={500}
|
||||
classNames="alert-animated">
|
||||
{this.renderAlert(alert)}
|
||||
</CSSTransition>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlertsContainer.propTypes = {
|
||||
alerts: arrayOf(
|
||||
shape({
|
||||
message: string.isRequired,
|
||||
type: string.isRequired,
|
||||
id: string.isRequired,
|
||||
timeout: number,
|
||||
onClose: func
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
clearAlert: func.isRequired
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ alerts }) => ({ alerts });
|
||||
|
||||
export default connect(mapStateToProps, { clearAlert })(AlertsContainer);
|
|
@ -55,7 +55,7 @@ class InviteUsersModal extends Component {
|
|||
team_id: this.props.team.id
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.setState({ inviteResults: data, showInviteUsersResults: true});
|
||||
this.setState({ inviteResults: data, showInviteUsersResults: true });
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("Invite As Error: ", error);
|
||||
|
@ -89,17 +89,23 @@ class InviteUsersModal extends Component {
|
|||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id="invite_users.modal_title"
|
||||
values={{ team: this.props.team.name }}
|
||||
values={{
|
||||
team: this.props.team.name
|
||||
}}
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>{modalBody}</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<StyledButtonToolbar>
|
||||
<Button onClick={this.handleCloseModal}>
|
||||
<FormattedMessage id="general.cancel" />
|
||||
</Button>
|
||||
{inviteButton}
|
||||
<Button onClick={this.handleCloseModal}>
|
||||
<FormattedMessage
|
||||
id={`general.${this.state.showInviteUsersResults
|
||||
? "close"
|
||||
: "cancel"}`}
|
||||
/>
|
||||
</Button>
|
||||
</StyledButtonToolbar>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
@ -110,10 +116,7 @@ class InviteUsersModal extends Component {
|
|||
InviteUsersModal.propTypes = {
|
||||
showModal: bool.isRequired,
|
||||
onCloseModal: func.isRequired,
|
||||
team: shape({
|
||||
id: number.isRequired,
|
||||
name: string.isRequired
|
||||
}).isRequired,
|
||||
team: shape({ id: number.isRequired, name: string.isRequired }).isRequired,
|
||||
updateUsersCallback: func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import UserAccountDropdown from "./components/UserAccountDropdown";
|
|||
const StyledNavbar = styled(Navbar)`
|
||||
background-color: ${WHITE_COLOR};
|
||||
border-color: ${BORDER_GRAY_COLOR};
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
const StyledBrand = styled.a`
|
||||
|
|
29
app/javascript/src/components/actions/AlertsActions.js
Normal file
29
app/javascript/src/components/actions/AlertsActions.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import shortid from "shortid";
|
||||
import {
|
||||
ADD_ALERT,
|
||||
CLEAR_ALERT,
|
||||
CLEAR_ALL_ALERTS
|
||||
} from "../../config/action_types";
|
||||
|
||||
export function addAlert(message,
|
||||
type,
|
||||
id = shortid.generate(),
|
||||
timeout = 5000) {
|
||||
return {
|
||||
payload: {
|
||||
message,
|
||||
type,
|
||||
id,
|
||||
timeout
|
||||
},
|
||||
type: ADD_ALERT
|
||||
};
|
||||
}
|
||||
|
||||
export function clearAlert(id) {
|
||||
return { payload: id, type: CLEAR_ALERT }
|
||||
}
|
||||
|
||||
export function clearAllAlerts() {
|
||||
return { type: CLEAR_ALL_ALERTS };
|
||||
}
|
31
app/javascript/src/components/reducers/AlertsReducers.js
Normal file
31
app/javascript/src/components/reducers/AlertsReducers.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import {
|
||||
ADD_ALERT,
|
||||
CLEAR_ALERT,
|
||||
CLEAR_ALL_ALERTS
|
||||
} from "../../config/action_types";
|
||||
|
||||
export const alerts = (
|
||||
state = [],
|
||||
action
|
||||
) => {
|
||||
switch(action.type) {
|
||||
case ADD_ALERT:
|
||||
return [
|
||||
...state,
|
||||
{
|
||||
message: action.payload.message,
|
||||
type: action.payload.type,
|
||||
id: action.payload.id,
|
||||
timeout: action.payload.timeout
|
||||
}
|
||||
];
|
||||
case CLEAR_ALERT:
|
||||
return state.filter((alert) => (
|
||||
alert.id !== action.payload
|
||||
));
|
||||
case CLEAR_ALL_ALERTS:
|
||||
return [];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -35,3 +35,8 @@ export const UPDATE_TEAM_DESCRIPTION_MODAL = "UPDATE_TEAM_DESCRIPTION_MODAL";
|
|||
// spinner
|
||||
export const SPINNER_ON = "SPINNER_ON";
|
||||
export const SPINNER_OFF = "SPINNER_OFF";
|
||||
|
||||
// alerts
|
||||
export const ADD_ALERT = "ADD_ALERT";
|
||||
export const CLEAR_ALERT = "CLEAR_ALERT";
|
||||
export const CLEAR_ALL_ALERTS = "CLEAR_ALL_ALERTS";
|
|
@ -6,11 +6,13 @@ import {
|
|||
} from "../components/reducers/TeamReducers";
|
||||
import { globalActivities } from "../components/reducers/ActivitiesReducers";
|
||||
import { currentUser } from "../components/reducers/UsersReducer";
|
||||
import { alerts } from "../components/reducers/AlertsReducers";
|
||||
|
||||
export default combineReducers({
|
||||
current_team: setCurrentTeam,
|
||||
all_teams: getListOfTeams,
|
||||
global_activities: globalActivities,
|
||||
current_user: currentUser,
|
||||
showLeaveTeamModal
|
||||
showLeaveTeamModal,
|
||||
alerts
|
||||
});
|
||||
|
|
|
@ -3,11 +3,13 @@ import { BrowserRouter } from "react-router-dom";
|
|||
import { Provider } from "react-redux";
|
||||
import { IntlProvider, addLocaleData } from "react-intl";
|
||||
import enLocaleData from "react-intl/locale-data/en";
|
||||
import styled from "styled-components";
|
||||
import { flattenMessages } from "./config/locales/utils";
|
||||
import messages from "./config/locales/messages";
|
||||
import store from "./config/store";
|
||||
|
||||
import Spinner from "./components/Spinner";
|
||||
import AlertsContainer from "./components/AlertsContainer";
|
||||
import ModalsContainer from "./components/ModalsContainer";
|
||||
import SettingsPage from "./scenes/SettingsPage";
|
||||
import Navigation from "./components/Navigation";
|
||||
|
@ -15,14 +17,22 @@ import Navigation from "./components/Navigation";
|
|||
addLocaleData([...enLocaleData]);
|
||||
const locale = "en-US";
|
||||
|
||||
export default () =>
|
||||
const ContentWrapper = styled.div`
|
||||
margin-top: 15px;
|
||||
`;
|
||||
|
||||
const ScinoteApp = () =>
|
||||
<Provider store={store}>
|
||||
<IntlProvider locale={locale} messages={flattenMessages(messages[locale])}>
|
||||
<IntlProvider locale={locale}
|
||||
messages={flattenMessages(messages[locale])}>
|
||||
<div>
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Navigation />
|
||||
<SettingsPage />
|
||||
<AlertsContainer />
|
||||
<ContentWrapper>
|
||||
<SettingsPage />
|
||||
</ContentWrapper>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
|
||||
|
@ -31,3 +41,5 @@ export default () =>
|
|||
</div>
|
||||
</IntlProvider>
|
||||
</Provider>;
|
||||
|
||||
export default ScinoteApp;
|
||||
|
|
30
app/javascript/src/styles/animations.scss
Normal file
30
app/javascript/src/styles/animations.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
.alert-animated-enter {
|
||||
opacity: .01;
|
||||
|
||||
&.alert-animated-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 150ms ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-animated-exit {
|
||||
opacity: 1;
|
||||
padding-bottom: 15px;
|
||||
padding-top: 15px;
|
||||
margin-bottom: 20px;
|
||||
height: 50px;
|
||||
|
||||
&.alert-animated-exit-active {
|
||||
overflow: hidden;
|
||||
opacity: .01;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
height: 0;
|
||||
transition: opacity 300ms ease-in,
|
||||
padding-top 500ms ease-in,
|
||||
padding-bottom 500ms ease-in,
|
||||
margin-bottom 500ms ease-in,
|
||||
height 500ms ease-in;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
@import 'constants';
|
||||
@import 'animations';
|
||||
@import 'react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css';
|
||||
@import '~react-bootstrap-table/dist/react-bootstrap-table.min';
|
||||
@import 'react-tagsinput/react-tagsinput.css';
|
||||
|
@ -14,6 +15,10 @@ body {
|
|||
background-color: $color-theme-primary;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 1.5em;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: $color-theme-secondary;
|
||||
border-color: $primary-hover-color;
|
||||
|
@ -30,3 +35,23 @@ body {
|
|||
position: relative !important;
|
||||
}
|
||||
}
|
||||
|
||||
// tags input
|
||||
.react-tagsinput--focused {
|
||||
border-color: $color-theme-primary;
|
||||
}
|
||||
|
||||
.react-tagsinput-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-tagsinput-tag {
|
||||
background-color: $color-theme-primary;
|
||||
border: 0;
|
||||
color: $color-white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.react-tagsinput-remove {
|
||||
color: $color-white;
|
||||
}
|
||||
|
|
|
@ -68,6 +68,11 @@
|
|||
"react-bootstrap-table": "^4.0.0",
|
||||
"react-bootstrap-timezone-picker": "^1.0.11",
|
||||
"react-data-grid": "^2.0.2",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"react-tagsinput": "^3.17.0",
|
||||
"react-transition-group": "^2.2.0",
|
||||
>>>>>>> fcea55c2a102470bd4520ea0ac1ef771710c17d8
|
||||
"react-dom": "^15.6.1",
|
||||
"react-intl": "^2.3.0",
|
||||
"react-intl-redux": "^0.6.0",
|
||||
|
@ -82,6 +87,7 @@
|
|||
"redux-thunk": "^2.2.0",
|
||||
"resolve-url-loader": "^2.1.0",
|
||||
"sass-loader": "^6.0.6",
|
||||
"shortid": "^2.2.8",
|
||||
"style-loader": "^0.18.2",
|
||||
"styled-components": "^2.1.1",
|
||||
"webpack": "^3.2.0",
|
||||
|
|
|
@ -6,8 +6,10 @@ describe ClientApi::Teams::TeamsController, type: :controller do
|
|||
before do
|
||||
@user_one = User.first
|
||||
@user_two = FactoryGirl.create :user, email: 'sec_user@asdf.com'
|
||||
@team_one = FactoryGirl.create :team
|
||||
@team_two = FactoryGirl.create :team, name: 'Team two'
|
||||
@team_one = FactoryGirl.create :team, created_by: @user_one
|
||||
@team_two = FactoryGirl.create :team,
|
||||
name: 'Team two',
|
||||
created_by: @user_two
|
||||
FactoryGirl.create :user_team, team: @team_one, user: @user_one, role: 2
|
||||
end
|
||||
|
||||
|
@ -19,6 +21,45 @@ describe ClientApi::Teams::TeamsController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
before do
|
||||
@team_one.update_attribute(:name, 'My Team')
|
||||
@team_one.update_attribute(:description, 'Lorem ipsum ipsum')
|
||||
end
|
||||
|
||||
it 'should return HTTP success response' do
|
||||
post :create, params: { team: @team_one }, as: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'should return HTTP unprocessable_entity response if name too short' do
|
||||
@team_one.update_attribute(
|
||||
:name,
|
||||
('a' * (Constants::NAME_MIN_LENGTH - 1)).to_s
|
||||
)
|
||||
post :create, params: { team: @team_one }, as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'should return HTTP unprocessable_entity response if name too long' do
|
||||
@team_one.update_attribute(
|
||||
:name,
|
||||
('a' * (Constants::NAME_MAX_LENGTH + 1)).to_s
|
||||
)
|
||||
post :create, params: { team: @team_one }, as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'should return HTTP unprocessable_entity response if description too long' do
|
||||
@team_one.update_attribute(
|
||||
:description,
|
||||
('a' * (Constants::TEXT_MAX_LENGTH + 1)).to_s
|
||||
)
|
||||
post :create, params: { team: @team_one }, as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #change_team' do
|
||||
it 'should return HTTP success response' do
|
||||
FactoryGirl.create :user_team, team: @team_two, user: @user_one, role: 2
|
||||
|
|
75
spec/services/client_api/teams/create_service_spec.rb
Normal file
75
spec/services/client_api/teams/create_service_spec.rb
Normal file
|
@ -0,0 +1,75 @@
|
|||
require 'rails_helper'
|
||||
|
||||
include ClientApi::Teams
|
||||
|
||||
describe ClientApi::Teams::CreateService do
|
||||
let(:user) { create :user, email: 'user@asdf.com' }
|
||||
let(:team) do
|
||||
build :team, name: 'My Team', description: 'My Description'
|
||||
end
|
||||
|
||||
it 'should raise a StandardError if current_user is not assigned' do
|
||||
expect { CreateService.new }.to raise_error(StandardError)
|
||||
end
|
||||
|
||||
it 'should create a new team' do
|
||||
service = CreateService.new(
|
||||
current_user: user,
|
||||
params: { name: team.name, description: team.description }
|
||||
)
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :success
|
||||
|
||||
team_n = Team.order(created_at: :desc).first
|
||||
expect(team_n.name).to eq team.name
|
||||
expect(team_n.description).to eq team.description
|
||||
expect(team_n.created_by).to eq user
|
||||
expect(team_n.users.count).to eq 1
|
||||
expect(team_n.users.take).to eq user
|
||||
end
|
||||
|
||||
it 'should return error response if params = {}' do
|
||||
service = CreateService.new(current_user: user, params: {})
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :error
|
||||
end
|
||||
|
||||
it 'should return error response if params are missing :name attribute' do
|
||||
service = CreateService.new(
|
||||
current_user: user,
|
||||
params: { description: team.description }
|
||||
)
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :error
|
||||
end
|
||||
|
||||
it 'should return error response if name too short' do
|
||||
team.name = ('a' * (Constants::NAME_MIN_LENGTH - 1)).to_s
|
||||
service = CreateService.new(
|
||||
current_user: user,
|
||||
params: { name: team.name, description: team.description }
|
||||
)
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :error
|
||||
end
|
||||
|
||||
it 'should return error response if name too long' do
|
||||
team.name = ('a' * (Constants::NAME_MAX_LENGTH + 1)).to_s
|
||||
service = CreateService.new(
|
||||
current_user: user,
|
||||
params: { name: team.name, description: team.description }
|
||||
)
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :error
|
||||
end
|
||||
|
||||
it 'should return error response if description too long' do
|
||||
team.description = ('a' * (Constants::TEXT_MAX_LENGTH + 1)).to_s
|
||||
service = CreateService.new(
|
||||
current_user: user,
|
||||
params: { name: team.name, description: team.description }
|
||||
)
|
||||
result = service.execute
|
||||
expect(result[:status]).to eq :error
|
||||
end
|
||||
end
|
19
yarn.lock
19
yarn.lock
|
@ -1156,6 +1156,10 @@ center-align@^0.1.1:
|
|||
align-text "^0.1.3"
|
||||
lazy-cache "^1.0.3"
|
||||
|
||||
chain-function@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc"
|
||||
|
||||
chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
|
||||
|
@ -4825,6 +4829,17 @@ react-timezone@^0.2.0:
|
|||
dependencies:
|
||||
classnames "^2.2.1"
|
||||
|
||||
react-transition-group@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.0.tgz#793bf8cb15bfe91b3101b24bce1c1d2891659575"
|
||||
dependencies:
|
||||
chain-function "^1.0.0"
|
||||
classnames "^2.2.5"
|
||||
dom-helpers "^3.2.0"
|
||||
loose-envify "^1.3.1"
|
||||
prop-types "^15.5.8"
|
||||
warning "^3.0.0"
|
||||
|
||||
react@^15.6.1:
|
||||
version "15.6.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df"
|
||||
|
@ -5298,6 +5313,10 @@ shelljs@^0.7.5:
|
|||
interpret "^1.0.0"
|
||||
rechoir "^0.6.2"
|
||||
|
||||
shortid@^2.2.8:
|
||||
version "2.2.8"
|
||||
resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131"
|
||||
|
||||
signal-exit@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||
|
|
Loading…
Add table
Reference in a new issue