diff --git a/.eslintrc b/.eslintrc index 2baf261..386b2b2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -34,6 +34,7 @@ ], "no-param-reassign": ["error", { "props": true, "ignorePropertyModificationsFor": ["state"] }], "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], + "react/require-default-props": "off", "import/no-unresolved": ["off"], "import/no-absolute-path": ["off"], "import/order": ["off"], diff --git a/Gemfile b/Gemfile index 54656c0..9ed6f8d 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,7 @@ gem 'bcrypt', '~> 3.1.7' gem 'bootsnap', '>= 1.4.2', require: false gem 'active_model_serializers' +gem 'js-routes' gem 'kaminari' gem 'ransack' gem 'responders' diff --git a/Gemfile.lock b/Gemfile.lock index 4300873..4cfa6aa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,6 +98,8 @@ GEM jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) + js-routes (2.2.4) + railties (>= 4) json (2.6.2) jsonapi-renderer (0.2.2) kaminari (1.2.2) @@ -278,6 +280,7 @@ DEPENDENCIES capybara (>= 2.15) factory_bot_rails jbuilder (~> 2.7) + js-routes kaminari listen (~> 3.2) pg (>= 0.18, < 2.0) diff --git a/app/controllers/api/v1/tasks_controller.rb b/app/controllers/api/v1/tasks_controller.rb index 0bac8d0..69137a8 100644 --- a/app/controllers/api/v1/tasks_controller.rb +++ b/app/controllers/api/v1/tasks_controller.rb @@ -16,6 +16,7 @@ def index end def create + params['task']['author_id'] = current_user.id task = current_user.my_tasks.new(task_params) task.save diff --git a/app/javascript/forms/TaskForm.js b/app/javascript/forms/TaskForm.js new file mode 100644 index 0000000..27ec634 --- /dev/null +++ b/app/javascript/forms/TaskForm.js @@ -0,0 +1,21 @@ +import { pick, propOr } from 'ramda'; + +export default { + defaultAttributes(attributes) { + return { + name: '', + description: '', + ...attributes, + }; + }, + + attributesToSubmit(task) { + const pertmittedKeys = ['id', 'name', 'description']; + + return { + ...pick(pertmittedKeys, task), + assigneeId: propOr(null, 'id', task.assignee), + authorId: propOr(null, 'id', task.author), + }; + }, +}; diff --git a/app/javascript/packs/components/AddPopup/AddPopup.js b/app/javascript/packs/components/AddPopup/AddPopup.js new file mode 100644 index 0000000..8cb9c42 --- /dev/null +++ b/app/javascript/packs/components/AddPopup/AddPopup.js @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { has } from 'ramda'; + +import Button from '@material-ui/core/Button'; +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; +import CloseIcon from '@material-ui/icons/Close'; +import IconButton from '@material-ui/core/IconButton'; +import Modal from '@material-ui/core/Modal'; +import TextField from '@material-ui/core/TextField'; + +import TaskForm from 'forms/TaskForm'; + +import useStyles from './useStyles'; + +function AddPopup({ onClose, onCardCreate }) { + const [task, changeTask] = useState(TaskForm.defaultAttributes()); + const [isSaving, setSaving] = useState(false); + const [errors, setErrors] = useState({}); + const handleCreate = () => { + setSaving(true); + + onCardCreate(task).catch((error) => { + setSaving(false); + setErrors(error || {}); + + if (error instanceof Error) { + alert(`Creation Failed! Error: ${error.message}`); + } + }); + }; + const handleChangeTextField = (fieldName) => (event) => changeTask({ ...task, [fieldName]: event.target.value }); + const styles = useStyles(); + + return ( + + + + + + } + title="Add New Task" + /> + +
+ + +
+
+ + + +
+
+ ); +} + +AddPopup.propTypes = { + onClose: PropTypes.func.isRequired, + onCardCreate: PropTypes.func.isRequired, +}; + +export default AddPopup; diff --git a/app/javascript/packs/components/AddPopup/index.js b/app/javascript/packs/components/AddPopup/index.js new file mode 100644 index 0000000..d0f632d --- /dev/null +++ b/app/javascript/packs/components/AddPopup/index.js @@ -0,0 +1,3 @@ +import AddPopup from './AddPopup'; + +export default AddPopup; diff --git a/app/javascript/packs/components/AddPopup/useStyles.js b/app/javascript/packs/components/AddPopup/useStyles.js new file mode 100644 index 0000000..f2405b0 --- /dev/null +++ b/app/javascript/packs/components/AddPopup/useStyles.js @@ -0,0 +1,26 @@ +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles(() => ({ + modal: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + outline: 0, + }, + + root: { + width: 465, + }, + + actions: { + display: 'flex', + justifyContent: 'right', + }, + + form: { + display: 'flex', + flexDirection: 'column', + }, +})); + +export default useStyles; diff --git a/app/javascript/packs/components/ColumnHeader/ColumnHeader.js b/app/javascript/packs/components/ColumnHeader/ColumnHeader.js new file mode 100644 index 0000000..7be18b5 --- /dev/null +++ b/app/javascript/packs/components/ColumnHeader/ColumnHeader.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import IconButton from '@material-ui/core/IconButton'; +import SystemUpdateAltIcon from '@material-ui/icons/SystemUpdateAlt'; + +import useStyles from './useStyles'; + +function ColumnHeader({ column, onLoadMore }) { + const styles = useStyles(); + + const { + id, + title, + cards, + meta: { totalCount, currentPage, totalPages }, + } = column; + + const count = cards.length; + + const handleLoadMore = () => onLoadMore(id, currentPage + 1); + + return ( +
+
+ {title} ({count}/{totalCount || '…'}) +
+
+ {currentPage >= totalPages ? null : ( + handleLoadMore()}> + + + )} +
+
+ ); +} + +ColumnHeader.propTypes = { + column: PropTypes.shape({ + id: PropTypes.string, + title: PropTypes.string, + cards: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + meta: PropTypes.shape({ + totalCount: PropTypes.number, + currentPage: PropTypes.number, + totalPages: PropTypes.number, + }), + }), + onLoadMore: PropTypes.func, +}; + +export default ColumnHeader; diff --git a/app/javascript/packs/components/ColumnHeader/index.js b/app/javascript/packs/components/ColumnHeader/index.js new file mode 100644 index 0000000..14babfd --- /dev/null +++ b/app/javascript/packs/components/ColumnHeader/index.js @@ -0,0 +1,3 @@ +import ColumnHeader from './ColumnHeader'; + +export default ColumnHeader; diff --git a/app/javascript/packs/components/ColumnHeader/useStyles.js b/app/javascript/packs/components/ColumnHeader/useStyles.js new file mode 100644 index 0000000..a5eb578 --- /dev/null +++ b/app/javascript/packs/components/ColumnHeader/useStyles.js @@ -0,0 +1,12 @@ +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles(() => ({ + root: { + height: 42, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + }, +})); + +export default useStyles; diff --git a/app/javascript/packs/components/EditPopup/EditPopup.js b/app/javascript/packs/components/EditPopup/EditPopup.js new file mode 100644 index 0000000..66d4701 --- /dev/null +++ b/app/javascript/packs/components/EditPopup/EditPopup.js @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { isNil } from 'ramda'; + +import Button from '@material-ui/core/Button'; +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import CardHeader from '@material-ui/core/CardHeader'; +import CloseIcon from '@material-ui/icons/Close'; +import IconButton from '@material-ui/core/IconButton'; +import Modal from '@material-ui/core/Modal'; + +import Form from './components/Form'; + +import useStyles from './useStyles'; + +function EditPopup({ cardId, onClose, onCardDestroy, onCardLoad, onCardUpdate }) { + const [task, setTask] = useState(null); + const [isSaving, setSaving] = useState(false); + const [errors, setErrors] = useState({}); + const styles = useStyles(); + + useEffect(() => { + onCardLoad(cardId).then(setTask); + }, []); + + const handleCardUpdate = () => { + setSaving(true); + + onCardUpdate(task).catch((error) => { + setSaving(false); + setErrors(error || {}); + + if (error instanceof Error) { + alert(`Update Failed! Error: ${error.message}`); + } + }); + }; + + const handleCardDestroy = () => { + setSaving(true); + + onCardDestroy(task).catch((error) => { + setSaving(false); + + alert(`Destrucion Failed! Error: ${error.message}`); + }); + }; + const isLoading = isNil(task); + + return ( + + + + + + } + title={isLoading ? 'Your task is loading. Please be patient.' : `Task # ${task.id} [${task.name}]`} + /> + + {isLoading ? ( +
+ +
+ ) : ( +
+ )} + + + + + + + + ); +} + +EditPopup.propTypes = { + cardId: PropTypes.number.isRequired, + onClose: PropTypes.func.isRequired, + onCardDestroy: PropTypes.func.isRequired, + onCardLoad: PropTypes.func.isRequired, + onCardUpdate: PropTypes.func.isRequired, +}; + +export default EditPopup; diff --git a/app/javascript/packs/components/EditPopup/components/Form.js b/app/javascript/packs/components/EditPopup/components/Form.js new file mode 100644 index 0000000..47b121c --- /dev/null +++ b/app/javascript/packs/components/EditPopup/components/Form.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { has } from 'ramda'; + +import TextField from '@material-ui/core/TextField'; + +import useStyles from './useStyles'; + +function Form({ errors, onChange, task }) { + const handleChangeTextField = (fieldName) => (event) => onChange({ ...task, [fieldName]: event.target.value }); + const styles = useStyles(); + + return ( + + + + + ); +} + +Form.propTypes = { + onChange: PropTypes.func.isRequired, + task: PropTypes.shape().isRequired, + errors: PropTypes.shape({ + name: PropTypes.arrayOf(PropTypes.string), + description: PropTypes.arrayOf(PropTypes.string), + author: PropTypes.arrayOf(PropTypes.string), + assignee: PropTypes.arrayOf(PropTypes.string), + }), +}; + +Form.defaultProps = { + errors: {}, +}; + +export default Form; diff --git a/app/javascript/packs/components/EditPopup/components/useStyles.js b/app/javascript/packs/components/EditPopup/components/useStyles.js new file mode 100644 index 0000000..8475a5d --- /dev/null +++ b/app/javascript/packs/components/EditPopup/components/useStyles.js @@ -0,0 +1,10 @@ +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(() => ({ + root: { + display: 'flex', + flexDirection: 'column', + }, +})); + +export default useStyles; diff --git a/app/javascript/packs/components/EditPopup/index.js b/app/javascript/packs/components/EditPopup/index.js new file mode 100644 index 0000000..4d20f3d --- /dev/null +++ b/app/javascript/packs/components/EditPopup/index.js @@ -0,0 +1,3 @@ +import EditPopup from './EditPopup'; + +export default EditPopup; diff --git a/app/javascript/packs/components/EditPopup/useStyles.js b/app/javascript/packs/components/EditPopup/useStyles.js new file mode 100644 index 0000000..01fdc0b --- /dev/null +++ b/app/javascript/packs/components/EditPopup/useStyles.js @@ -0,0 +1,26 @@ +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(() => ({ + modal: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + outline: 0, + }, + + root: { + width: 465, + }, + + loader: { + display: 'flex', + justifyContent: 'center', + }, + + actions: { + display: 'flex', + justifyContent: 'flex-end', + }, +})); + +export default useStyles; diff --git a/app/javascript/packs/components/Task/Task.js b/app/javascript/packs/components/Task/Task.js new file mode 100644 index 0000000..bfb3fe0 --- /dev/null +++ b/app/javascript/packs/components/Task/Task.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Card from '@material-ui/core/Card'; +import CardHeader from '@material-ui/core/CardHeader'; +import CardContent from '@material-ui/core/CardContent'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import EditIcon from '@material-ui/icons/Edit'; + +import useStyles from './useStyles'; + +function Task({ task, onClick }) { + const styles = useStyles; + const handleClick = () => onClick(task); + const action = ( + + + + ); + + return ( + + + + + {task.description} + + + + ); +} + +Task.propTypes = { + task: PropTypes.shape().isRequired, + onClick: PropTypes.func.isRequired, +}; + +export default Task; diff --git a/app/javascript/packs/components/Task/index.js b/app/javascript/packs/components/Task/index.js new file mode 100644 index 0000000..6dc333e --- /dev/null +++ b/app/javascript/packs/components/Task/index.js @@ -0,0 +1,3 @@ +import Task from './Task'; + +export default Task; diff --git a/app/javascript/packs/components/Task/useStyles.js b/app/javascript/packs/components/Task/useStyles.js new file mode 100644 index 0000000..44ab8b7 --- /dev/null +++ b/app/javascript/packs/components/Task/useStyles.js @@ -0,0 +1,9 @@ +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles(() => ({ + root: { + width: 250, + }, +})); + +export default useStyles; diff --git a/app/javascript/packs/components/TaskBoard/TaskBoard.js b/app/javascript/packs/components/TaskBoard/TaskBoard.js index 08ece16..e01e3ee 100644 --- a/app/javascript/packs/components/TaskBoard/TaskBoard.js +++ b/app/javascript/packs/components/TaskBoard/TaskBoard.js @@ -1,36 +1,177 @@ -import React from 'react'; -import Board from '@asseinfo/react-kanban'; -import '@asseinfo/react-kanban/dist/styles.css'; - -const data = { - columns: [ - { - id: 1, - title: 'Backlog', - cards: [ - { - id: 1, - title: 'Add card', - description: 'Add capability to add a card in a column', - }, - ], - }, - { - id: 2, - title: 'Doing', - cards: [ - { - id: 2, - title: 'Drag-n-drop support', - description: 'Move a card between the columns', - }, - ], - }, - ], +import React, { useEffect, useState } from 'react'; +import KanbanBoard from '@asseinfo/react-kanban'; +import { propOr } from 'ramda'; +import Fab from '@material-ui/core/Fab'; +import AddIcon from '@material-ui/icons/Add'; + +import Task from 'packs/components/Task'; +import AddPopup from 'packs/components/AddPopup'; +import EditPopup from 'packs/components/EditPopup'; +import ColumnHeader from 'packs/components/ColumnHeader'; +import useStyles from './useStyles'; +import TaskForm from 'forms/TaskForm'; +import TasksRepository from 'repositories/TasksRepository'; + +const STATES = [ + { key: 'new_task', value: 'New' }, + { key: 'in_development', value: 'In Dev' }, + { key: 'in_qa', value: 'In QA' }, + { key: 'in_code_review', value: 'in CR' }, + { key: 'ready_for_release', value: 'Ready for release' }, + { key: 'released', value: 'Released' }, + { key: 'archived', value: 'Archived' }, +]; + +const MODES = { + ADD: 'add', + NONE: 'none', + EDIT: 'edit', +}; + +const initialBoard = { + columns: STATES.map((column) => ({ + id: column.key, + title: column.value, + cards: [], + meta: {}, + })), }; function TaskBoard() { - return ; + const [board, setBoard] = useState(initialBoard); + const [boardCards, setBoardCards] = useState([]); + const [mode, setMode] = useState(MODES.NONE); + const [openedTaskId, setOpenedTaskId] = useState(null); + useEffect(() => loadBoard(), []); + useEffect(() => generateBoard(), [boardCards]); + + const loadColumn = (state, page, perPage) => + TasksRepository.index({ + q: { stateEq: state, s: 'id DESC' }, + page, + perPage, + }); + + const loadColumnInitial = (state, page = 1, perPage = 10) => { + loadColumn(state, page, perPage).then(({ data }) => { + setBoardCards((prevState) => ({ + ...prevState, + [state]: { cards: data.items, meta: data.meta }, + })); + }); + }; + + const loadColumnMore = (state, page = 1, perPage = 10) => { + loadColumn(state, page, perPage).then(({ data }) => { + setBoardCards((prevState) => ({ + ...prevState, + [state]: { cards: [...prevState[state].cards, ...data.items], meta: data.meta }, + })); + }); + }; + + const generateBoard = () => { + const board = { + columns: STATES.map(({ key, value }) => ({ + id: key, + title: value, + cards: propOr({}, 'cards', boardCards[key]), + meta: propOr({}, 'meta', boardCards[key]), + })), + }; + + setBoard(board); + }; + + const loadBoard = () => { + STATES.map(({ key }) => loadColumnInitial(key)); + }; + + const handleCardDragEnd = (task, source, destination) => { + const transition = task.transitions.find(({ to }) => destination.toColumnId === to); + if (!transition) { + return null; + } + + return TasksRepository.update(task.id, { stateEvent: transition.event }) + .then(() => { + loadColumnInitial(destination.toColumnId); + loadColumnInitial(source.fromColumnId); + }) + .catch((error) => { + alert(`Move failed! ${error.message}`); + }); + }; + + const handleAddPopupOpen = () => { + setMode(MODES.ADD); + }; + + const handleClose = () => { + setMode(MODES.NONE); + }; + + const handleTaskCreate = (params) => { + const attributes = TaskForm.attributesToSubmit(params); + return TasksRepository.create(attributes).then(({ data: { task } }) => { + loadColumnInitial(task.state); + handleClose(); + }); + }; + + const styles = useStyles(); + + const loadTask = (id) => TasksRepository.show(id).then(({ data: { task } }) => task); + + const handleTaskUpdate = (task) => { + const attributes = TaskForm.attributesToSubmit(task); + + return TasksRepository.update(task.id, attributes).then(() => { + loadColumnInitial(task.state); + handleClose(); + }); + }; + + const handleTaskDestroy = (task) => + TasksRepository.destroy(task.id).then(() => { + loadColumnInitial(task.state); + handleClose(); + }); + + const handleEditPopupOpen = (task) => { + setOpenedTaskId(task.id); + setMode(MODES.EDIT); + }; + + const handleEditPopupClose = () => { + setMode(MODES.NONE); + setOpenedTaskId(null); + }; + + return ( + <> + } + renderColumnHeader={(column) => } + onCardDragEnd={handleCardDragEnd} + > + {board} + + {mode === MODES.ADD && } + {mode === MODES.EDIT && ( + + )} + + + + + ); } export default TaskBoard; diff --git a/app/javascript/packs/components/TaskBoard/useStyles.js b/app/javascript/packs/components/TaskBoard/useStyles.js new file mode 100644 index 0000000..df3697f --- /dev/null +++ b/app/javascript/packs/components/TaskBoard/useStyles.js @@ -0,0 +1,11 @@ +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles(() => ({ + addButton: { + position: 'fixed', + bottom: 32, + right: 32, + }, +})); + +export default useStyles; diff --git a/app/javascript/repositories/TasksRepository.js b/app/javascript/repositories/TasksRepository.js new file mode 100644 index 0000000..f4d251a --- /dev/null +++ b/app/javascript/repositories/TasksRepository.js @@ -0,0 +1,29 @@ +import routes from 'routes'; +import FetchHelper from 'utils/fetchHelper'; + +export default { + index(params) { + const path = routes.apiV1TasksPath(); + return FetchHelper.get(path, params); + }, + + show(id) { + const path = routes.apiV1TaskPath(id); + return FetchHelper.get(path); + }, + + update(id, task = {}) { + const path = routes.apiV1TaskPath(id); + return FetchHelper.put(path, { task }); + }, + + create(task = {}) { + const path = routes.apiV1TasksPath(); + return FetchHelper.post(path, { task }); + }, + + destroy(id) { + const path = routes.apiV1TaskPath(id); + return FetchHelper.delete(path); + }, +}; diff --git a/app/javascript/routes/ApiRoutes.js b/app/javascript/routes/ApiRoutes.js new file mode 100644 index 0000000..23abf7c --- /dev/null +++ b/app/javascript/routes/ApiRoutes.js @@ -0,0 +1,776 @@ +/** + * File generated by js-routes 2.2.4 + * Based on Rails 6.0.6 routes of App::Application + */ +const __jsr = (() => { + const hasProp = (value, key) => Object.prototype.hasOwnProperty.call(value, key); + let NodeTypes; + (function (NodeTypes) { + NodeTypes[NodeTypes["GROUP"] = 1] = "GROUP"; + NodeTypes[NodeTypes["CAT"] = 2] = "CAT"; + NodeTypes[NodeTypes["SYMBOL"] = 3] = "SYMBOL"; + NodeTypes[NodeTypes["OR"] = 4] = "OR"; + NodeTypes[NodeTypes["STAR"] = 5] = "STAR"; + NodeTypes[NodeTypes["LITERAL"] = 6] = "LITERAL"; + NodeTypes[NodeTypes["SLASH"] = 7] = "SLASH"; + NodeTypes[NodeTypes["DOT"] = 8] = "DOT"; + })(NodeTypes || (NodeTypes = {})); + const isBrowser = typeof window !== "undefined"; + const ModuleReferences = { + CJS: { + define(routes) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + module.exports = routes; + }, + isSupported() { + return typeof module === "object"; + }, + }, + AMD: { + define(routes) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + define([], function () { + return routes; + }); + }, + isSupported() { + return typeof define === "function" && !!define.amd; + }, + }, + UMD: { + define(routes) { + if (ModuleReferences.AMD.isSupported()) { + ModuleReferences.AMD.define(routes); + } + else { + if (ModuleReferences.CJS.isSupported()) { + try { + ModuleReferences.CJS.define(routes); + } + catch (error) { + if (error.name !== "TypeError") + throw error; + } + } + } + }, + isSupported() { + return (ModuleReferences.AMD.isSupported() || + ModuleReferences.CJS.isSupported()); + }, + }, + ESM: { + define() { + // Module can only be defined using ruby code generation + }, + isSupported() { + // Its impossible to check if "export" keyword is supported + return true; + }, + }, + NIL: { + define() { + // Defined using const __jsr = + }, + isSupported() { + return true; + }, + }, + DTS: { + // Acts the same as ESM + define(routes) { + ModuleReferences.ESM.define(routes); + }, + isSupported() { + return ModuleReferences.ESM.isSupported(); + }, + }, + }; + class ParametersMissing extends Error { + constructor(...keys) { + super(`Route missing required keys: ${keys.join(", ")}`); + this.keys = keys; + Object.setPrototypeOf(this, Object.getPrototypeOf(this)); + this.name = ParametersMissing.name; + } + } + const UriEncoderSegmentRegex = /[^a-zA-Z0-9\-._~!$&'()*+,;=:@]/g; + const ReservedOptions = [ + "anchor", + "trailing_slash", + "subdomain", + "host", + "port", + "protocol", + ]; + class UtilsClass { + constructor() { + this.configuration = { + prefix: "", + default_url_options: {}, + special_options_key: "_options", + serializer: null || this.default_serializer.bind(this), + }; + } + default_serializer(value, prefix) { + if (this.is_nullable(value)) { + return ""; + } + if (!prefix && !this.is_object(value)) { + throw new Error("Url parameters should be a javascript hash"); + } + prefix = prefix || ""; + const result = []; + if (this.is_array(value)) { + for (const element of value) { + result.push(this.default_serializer(element, prefix + "[]")); + } + } + else if (this.is_object(value)) { + for (let key in value) { + if (!hasProp(value, key)) + continue; + let prop = value[key]; + if (this.is_nullable(prop) && prefix) { + prop = ""; + } + if (this.is_not_nullable(prop)) { + if (prefix) { + key = prefix + "[" + key + "]"; + } + result.push(this.default_serializer(prop, key)); + } + } + } + else { + if (this.is_not_nullable(value)) { + result.push(encodeURIComponent(prefix) + "=" + encodeURIComponent("" + value)); + } + } + return result.join("&"); + } + serialize(object) { + return this.configuration.serializer(object); + } + extract_options(number_of_params, args) { + const last_el = args[args.length - 1]; + if ((args.length > number_of_params && last_el === 0) || + (this.is_object(last_el) && + !this.looks_like_serialized_model(last_el))) { + if (this.is_object(last_el)) { + delete last_el[this.configuration.special_options_key]; + } + return { + args: args.slice(0, args.length - 1), + options: last_el, + }; + } + else { + return { args, options: {} }; + } + } + looks_like_serialized_model(object) { + return (this.is_object(object) && + !(this.configuration.special_options_key in object) && + ("id" in object || "to_param" in object || "toParam" in object)); + } + path_identifier(object) { + const result = this.unwrap_path_identifier(object); + return this.is_nullable(result) || result === false ? "" : "" + result; + } + unwrap_path_identifier(object) { + let result = object; + if (!this.is_object(object)) { + return object; + } + if ("to_param" in object) { + result = object.to_param; + } + else if ("toParam" in object) { + result = object.toParam; + } + else if ("id" in object) { + result = object.id; + } + else { + result = object; + } + return this.is_callable(result) ? result.call(object) : result; + } + partition_parameters(parts, required_params, default_options, call_arguments) { + // eslint-disable-next-line prefer-const + let { args, options } = this.extract_options(parts.length, call_arguments); + if (args.length > parts.length) { + throw new Error("Too many parameters provided for path"); + } + let use_all_parts = args.length > required_params.length; + const parts_options = { + ...this.configuration.default_url_options, + }; + for (const key in options) { + const value = options[key]; + if (!hasProp(options, key)) + continue; + use_all_parts = true; + if (parts.includes(key)) { + parts_options[key] = value; + } + } + options = { + ...this.configuration.default_url_options, + ...default_options, + ...options, + }; + const keyword_parameters = {}; + let query_parameters = {}; + for (const key in options) { + if (!hasProp(options, key)) + continue; + const value = options[key]; + if (key === "params") { + if (this.is_object(value)) { + query_parameters = { + ...query_parameters, + ...value, + }; + } + else { + throw new Error("params value should always be an object"); + } + } + else if (this.is_reserved_option(key)) { + keyword_parameters[key] = value; + } + else { + if (!this.is_nullable(value) && + (value !== default_options[key] || required_params.includes(key))) { + query_parameters[key] = value; + } + } + } + const route_parts = use_all_parts ? parts : required_params; + let i = 0; + for (const part of route_parts) { + if (i < args.length) { + const value = args[i]; + if (!hasProp(parts_options, part)) { + query_parameters[part] = value; + ++i; + } + } + } + return { keyword_parameters, query_parameters }; + } + build_route(parts, required_params, default_options, route, absolute, args) { + const { keyword_parameters, query_parameters, } = this.partition_parameters(parts, required_params, default_options, args); + const missing_params = required_params.filter((param) => !hasProp(query_parameters, param) || + this.is_nullable(query_parameters[param])); + if (missing_params.length) { + throw new ParametersMissing(...missing_params); + } + let result = this.get_prefix() + this.visit(route, query_parameters); + if (keyword_parameters.trailing_slash) { + result = result.replace(/(.*?)[/]?$/, "$1/"); + } + const url_params = this.serialize(query_parameters); + if (url_params.length) { + result += "?" + url_params; + } + result += keyword_parameters.anchor + ? "#" + keyword_parameters.anchor + : ""; + if (absolute) { + result = this.route_url(keyword_parameters) + result; + } + return result; + } + visit(route, parameters, optional = false) { + switch (route[0]) { + case NodeTypes.GROUP: + return this.visit(route[1], parameters, true); + case NodeTypes.CAT: + return this.visit_cat(route, parameters, optional); + case NodeTypes.SYMBOL: + return this.visit_symbol(route, parameters, optional); + case NodeTypes.STAR: + return this.visit_globbing(route[1], parameters, true); + case NodeTypes.LITERAL: + case NodeTypes.SLASH: + case NodeTypes.DOT: + return route[1]; + default: + throw new Error("Unknown Rails node type"); + } + } + is_not_nullable(object) { + return !this.is_nullable(object); + } + is_nullable(object) { + return object === undefined || object === null; + } + visit_cat( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [_type, left, right], parameters, optional) { + const left_part = this.visit(left, parameters, optional); + let right_part = this.visit(right, parameters, optional); + if (optional && + ((this.is_optional_node(left[0]) && !left_part) || + (this.is_optional_node(right[0]) && !right_part))) { + return ""; + } + // if left_part ends on '/' and right_part starts on '/' + if (left_part[left_part.length - 1] === "/" && right_part[0] === "/") { + // strip slash from right_part + // to prevent double slash + right_part = right_part.substring(1); + } + return left_part + right_part; + } + visit_symbol( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [_type, key], parameters, optional) { + const value = this.path_identifier(parameters[key]); + delete parameters[key]; + if (value.length) { + return this.encode_segment(value); + } + if (optional) { + return ""; + } + else { + throw new ParametersMissing(key); + } + } + encode_segment(segment) { + return segment.replace(UriEncoderSegmentRegex, (str) => encodeURIComponent(str)); + } + is_optional_node(node) { + return [NodeTypes.STAR, NodeTypes.SYMBOL, NodeTypes.CAT].includes(node); + } + build_path_spec(route, wildcard = false) { + let key; + switch (route[0]) { + case NodeTypes.GROUP: + return "(" + this.build_path_spec(route[1]) + ")"; + case NodeTypes.CAT: + return (this.build_path_spec(route[1]) + this.build_path_spec(route[2])); + case NodeTypes.STAR: + return this.build_path_spec(route[1], true); + case NodeTypes.SYMBOL: + key = route[1]; + if (wildcard) { + return (key.startsWith("*") ? "" : "*") + key; + } + else { + return ":" + key; + } + break; + case NodeTypes.SLASH: + case NodeTypes.DOT: + case NodeTypes.LITERAL: + return route[1]; + default: + throw new Error("Unknown Rails node type"); + } + } + visit_globbing(route, parameters, optional) { + const key = route[1]; + let value = parameters[key]; + delete parameters[key]; + if (this.is_nullable(value)) { + return this.visit(route, parameters, optional); + } + if (this.is_array(value)) { + value = value.join("/"); + } + const result = this.path_identifier(value); + return false + ? result + : encodeURI(result); + } + get_prefix() { + const prefix = this.configuration.prefix; + return prefix.match("/$") + ? prefix.substring(0, prefix.length - 1) + : prefix; + } + route(parts_table, route_spec, absolute = false) { + const required_params = []; + const parts = []; + const default_options = {}; + for (const [part, { r: required, d: value }] of Object.entries(parts_table)) { + parts.push(part); + if (required) { + required_params.push(part); + } + if (this.is_not_nullable(value)) { + default_options[part] = value; + } + } + const result = (...args) => { + return this.build_route(parts, required_params, default_options, route_spec, absolute, args); + }; + result.requiredParams = () => required_params; + result.toString = () => { + return this.build_path_spec(route_spec); + }; + return result; + } + route_url(route_defaults) { + const hostname = route_defaults.host || this.current_host(); + if (!hostname) { + return ""; + } + const subdomain = route_defaults.subdomain + ? route_defaults.subdomain + "." + : ""; + const protocol = route_defaults.protocol || this.current_protocol(); + let port = route_defaults.port || + (!route_defaults.host ? this.current_port() : undefined); + port = port ? ":" + port : ""; + return protocol + "://" + subdomain + hostname + port; + } + current_host() { + var _a; + return (isBrowser && ((_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.hostname)) || ""; + } + current_protocol() { + var _a, _b; + return ((isBrowser && ((_b = (_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.protocol) === null || _b === void 0 ? void 0 : _b.replace(/:$/, ""))) || "http"); + } + current_port() { + var _a; + return (isBrowser && ((_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.port)) || ""; + } + is_object(value) { + return (typeof value === "object" && + Object.prototype.toString.call(value) === "[object Object]"); + } + is_array(object) { + return object instanceof Array; + } + is_callable(object) { + return typeof object === "function" && !!object.call; + } + is_reserved_option(key) { + return ReservedOptions.includes(key); + } + configure(new_config) { + this.configuration = { ...this.configuration, ...new_config }; + return this.configuration; + } + config() { + return { ...this.configuration }; + } + is_module_supported(name) { + return ModuleReferences[name].isSupported(); + } + ensure_module_supported(name) { + if (!this.is_module_supported(name)) { + throw new Error(`${name} is not supported by runtime`); + } + } + define_module(name, module) { + this.ensure_module_supported(name); + ModuleReferences[name].define(module); + } + } + const Utils = new UtilsClass(); + // We want this helper name to be short + const __jsr = { + r(parts_table, route_spec, absolute) { + return Utils.route(parts_table, route_spec, absolute); + }, + }; + const result = { + ...__jsr, + configure: (config) => { + return Utils.configure(config); + }, + config: () => { + return Utils.config(); + }, + serialize: (object) => { + return Utils.serialize(object); + }, + ...{}, + }; + Utils.define_module("ESM", result); + return result; +})(); +export const configure = __jsr.configure; + +export const config = __jsr.config; + +export const serialize = __jsr.serialize; + +/** + * Generates rails route to + * /admin/users/:id(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const adminUserPath = __jsr.r({"id":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"admin"],[2,[7,"/"],[2,[6,"users"],[2,[7,"/"],[2,[3,"id"],[1,[2,[8,"."],[3,"format"]]]]]]]]]); + +/** + * Generates rails route to + * /admin/users(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const adminUsersPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"admin"],[2,[7,"/"],[2,[6,"users"],[1,[2,[8,"."],[3,"format"]]]]]]]); + +/** + * Generates rails route to + * /api/v1/tasks/:id(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const apiV1TaskPath = __jsr.r({"id":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"api"],[2,[7,"/"],[2,[6,"v1"],[2,[7,"/"],[2,[6,"tasks"],[2,[7,"/"],[2,[3,"id"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]); + +/** + * Generates rails route to + * /api/v1/tasks(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const apiV1TasksPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"api"],[2,[7,"/"],[2,[6,"v1"],[2,[7,"/"],[2,[6,"tasks"],[1,[2,[8,"."],[3,"format"]]]]]]]]]); + +/** + * Generates rails route to + * /board(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const boardPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"board"],[1,[2,[8,"."],[3,"format"]]]]]); + +/** + * Generates rails route to + * /developers(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const developersPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"developers"],[1,[2,[8,"."],[3,"format"]]]]]); + +/** + * Generates rails route to + * /admin/users/:id/edit(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const editAdminUserPath = __jsr.r({"id":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"admin"],[2,[7,"/"],[2,[6,"users"],[2,[7,"/"],[2,[3,"id"],[2,[7,"/"],[2,[6,"edit"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const editRailsConductorInboundEmailPath = __jsr.r({"id":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"conductor"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[6,"inbound_emails"],[2,[7,"/"],[2,[3,"id"],[2,[7,"/"],[2,[6,"edit"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]]]); + +/** + * Generates rails route to + * /admin/users/new(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const newAdminUserPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"admin"],[2,[7,"/"],[2,[6,"users"],[2,[7,"/"],[2,[6,"new"],[1,[2,[8,"."],[3,"format"]]]]]]]]]); + +/** + * Generates rails route to + * /developers/new(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const newDeveloperPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"developers"],[2,[7,"/"],[2,[6,"new"],[1,[2,[8,"."],[3,"format"]]]]]]]); + +/** + * Generates rails route to + * /rails/conductor/action_mailbox/inbound_emails/new(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const newRailsConductorInboundEmailPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"conductor"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[6,"inbound_emails"],[2,[7,"/"],[2,[6,"new"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]); + +/** + * Generates rails route to + * /session/new(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const newSessionPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"session"],[2,[7,"/"],[2,[6,"new"],[1,[2,[8,"."],[3,"format"]]]]]]]); + +/** + * Generates rails route to + * /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) + * @param {any} signedBlobId + * @param {any} variationKey + * @param {any} filename + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsBlobRepresentationPath = __jsr.r({"signed_blob_id":{"r":true},"variation_key":{"r":true},"filename":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"active_storage"],[2,[7,"/"],[2,[6,"representations"],[2,[7,"/"],[2,[3,"signed_blob_id"],[2,[7,"/"],[2,[3,"variation_key"],[2,[7,"/"],[2,[5,[3,"filename"]],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/conductor/action_mailbox/inbound_emails/:id(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsConductorInboundEmailPath = __jsr.r({"id":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"conductor"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[6,"inbound_emails"],[2,[7,"/"],[2,[3,"id"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) + * @param {any} inboundEmailId + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsConductorInboundEmailReroutePath = __jsr.r({"inbound_email_id":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"conductor"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[3,"inbound_email_id"],[2,[7,"/"],[2,[6,"reroute"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/conductor/action_mailbox/inbound_emails(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsConductorInboundEmailsPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"conductor"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[6,"inbound_emails"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/active_storage/direct_uploads(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsDirectUploadsPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"active_storage"],[2,[7,"/"],[2,[6,"direct_uploads"],[1,[2,[8,"."],[3,"format"]]]]]]]]]); + +/** + * Generates rails route to + * /rails/active_storage/disk/:encoded_key/*filename(.:format) + * @param {any} encodedKey + * @param {any} filename + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsDiskServicePath = __jsr.r({"encoded_key":{"r":true},"filename":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"active_storage"],[2,[7,"/"],[2,[6,"disk"],[2,[7,"/"],[2,[3,"encoded_key"],[2,[7,"/"],[2,[5,[3,"filename"]],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/info(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsInfoPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"info"],[1,[2,[8,"."],[3,"format"]]]]]]]); + +/** + * Generates rails route to + * /rails/info/properties(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsInfoPropertiesPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"info"],[2,[7,"/"],[2,[6,"properties"],[1,[2,[8,"."],[3,"format"]]]]]]]]]); + +/** + * Generates rails route to + * /rails/info/routes(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsInfoRoutesPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"info"],[2,[7,"/"],[2,[6,"routes"],[1,[2,[8,"."],[3,"format"]]]]]]]]]); + +/** + * Generates rails route to + * /rails/mailers(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsMailersPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"mailers"],[1,[2,[8,"."],[3,"format"]]]]]]]); + +/** + * Generates rails route to + * /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsMailgunInboundEmailsPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[6,"mailgun"],[2,[7,"/"],[2,[6,"inbound_emails"],[2,[7,"/"],[2,[6,"mime"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/action_mailbox/mandrill/inbound_emails(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsMandrillInboundEmailsPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[6,"mandrill"],[2,[7,"/"],[2,[6,"inbound_emails"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/action_mailbox/mandrill/inbound_emails(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsMandrillInboundHealthCheckPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[6,"mandrill"],[2,[7,"/"],[2,[6,"inbound_emails"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/action_mailbox/postmark/inbound_emails(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsPostmarkInboundEmailsPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[6,"postmark"],[2,[7,"/"],[2,[6,"inbound_emails"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/action_mailbox/relay/inbound_emails(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsRelayInboundEmailsPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[6,"relay"],[2,[7,"/"],[2,[6,"inbound_emails"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/action_mailbox/sendgrid/inbound_emails(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsSendgridInboundEmailsPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"action_mailbox"],[2,[7,"/"],[2,[6,"sendgrid"],[2,[7,"/"],[2,[6,"inbound_emails"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]); + +/** + * Generates rails route to + * /rails/active_storage/blobs/:signed_id/*filename(.:format) + * @param {any} signedId + * @param {any} filename + * @param {object | undefined} options + * @returns {string} route path + */ +export const railsServiceBlobPath = __jsr.r({"signed_id":{"r":true},"filename":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"active_storage"],[2,[7,"/"],[2,[6,"blobs"],[2,[7,"/"],[2,[3,"signed_id"],[2,[7,"/"],[2,[5,[3,"filename"]],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]); + +/** + * Generates rails route to + * / + * @param {object | undefined} options + * @returns {string} route path + */ +export const rootPath = __jsr.r({}, [7,"/"]); + +/** + * Generates rails route to + * /session(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const sessionPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"session"],[1,[2,[8,"."],[3,"format"]]]]]); + +/** + * Generates rails route to + * /rails/active_storage/disk/:encoded_token(.:format) + * @param {any} encodedToken + * @param {object | undefined} options + * @returns {string} route path + */ +export const updateRailsDiskServicePath = __jsr.r({"encoded_token":{"r":true},"format":{}}, [2,[7,"/"],[2,[6,"rails"],[2,[7,"/"],[2,[6,"active_storage"],[2,[7,"/"],[2,[6,"disk"],[2,[7,"/"],[2,[3,"encoded_token"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]); + diff --git a/app/javascript/routes/index.js b/app/javascript/routes/index.js new file mode 100644 index 0000000..4d1fb88 --- /dev/null +++ b/app/javascript/routes/index.js @@ -0,0 +1,3 @@ +import * as routes from './ApiRoutes'; + +export default routes; diff --git a/app/javascript/utils/fetchHelper.js b/app/javascript/utils/fetchHelper.js new file mode 100644 index 0000000..537f9b0 --- /dev/null +++ b/app/javascript/utils/fetchHelper.js @@ -0,0 +1,67 @@ +import axios from 'axios'; +import qs from 'qs'; + +import { camelize, decamelize } from './keysConverter'; + +function authenticityToken() { + const token = document.querySelector('meta[name="csrf-token"]'); + return token ? token.content : null; +} + +function headers() { + return { + Accept: '*/*', + 'Content-Type': 'application/json', + 'X-CSRF-Token': authenticityToken(), + 'X-Requested-With': 'XMLHttpRequest', + }; +} + +axios.defaults.headers.post = headers(); +axios.defaults.headers.put = headers(); +axios.defaults.headers.delete = headers(); +axios.interceptors.response.use(null, (error) => { + if (error.response.status === 422) { + const { + response: { data: errors }, + } = error; + return Promise.reject(camelize(errors.errors)); + } + + if (error.response.status === 500) { + return Promise.reject(new Error('Something went wrong, please retry again')); + } + + return Promise.reject(error); +}); + +export default { + get(url, params = {}) { + return axios + .get(url, { + params: decamelize(params), + paramsSerializer: { + serialize: (parameters) => qs.stringify(parameters, { encode: false }), + }, + }) + .then(camelize); + }, + + post(url, json) { + const body = decamelize(json); + + return axios.post(url, body).then(camelize); + }, + + put(url, json) { + const body = decamelize(json); + + return axios.put(url, body).then(camelize); + }, + + delete(url, json) { + const body = decamelize(json); + + return axios.delete(url, body).then(camelize); + }, +}; diff --git a/app/javascript/utils/keysConverter.js b/app/javascript/utils/keysConverter.js new file mode 100644 index 0000000..b7238ee --- /dev/null +++ b/app/javascript/utils/keysConverter.js @@ -0,0 +1,4 @@ +import humps from 'humps'; + +export const decamelize = (obj) => humps.decamelizeKeys(obj); +export const camelize = (obj) => humps.camelizeKeys(obj); diff --git a/config/routes.rb b/config/routes.rb index 8b28397..8425736 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,7 +12,7 @@ end namespace :api do - namespace :v1 do + namespace :v1, defaults: { format: 'json' } do resources :tasks, only: %i[index show create update destroy] end end diff --git a/lib/tasks/js_routes.rake b/lib/tasks/js_routes.rake new file mode 100644 index 0000000..fa14992 --- /dev/null +++ b/lib/tasks/js_routes.rake @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'js-routes' + +ROUTES_DIR = File.join('app', 'javascript', 'routes') +namespace :js_routes do + desc 'Generate js routes for webpack' + task generate: :environment do + FileUtils.mkdir_p(Rails.root.join(ROUTES_DIR)) + file_name = File.join('routes', 'ApiRoutes.js') + JsRoutes.generate!(file_name, camel_case: true) + end +end diff --git a/lib/tasks/tasks_seed.rake b/lib/tasks/tasks_seed.rake new file mode 100644 index 0000000..a2c189f --- /dev/null +++ b/lib/tasks/tasks_seed.rake @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +namespace :tasks do + desc 'generates tasks' + task generate: :environment do + admin = Admin.find_or_create_by(first_name: 'admin', last_name: 'admin', email: 'admin@localhost.com') + 10.times do |i| + Task.create( + name: "card_name_#{i}", + description: "card_description_#{i}", + author: admin, + state: %w[new_task in_development in_qa in_codereview ready_for_release released archived].sample + ) + end + end +end diff --git a/package.json b/package.json index 4a830f9..35100d5 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,13 @@ "@asseinfo/react-kanban": "^2.2.0", "@babel/eslint-parser": "^7.19.1", "@babel/preset-react": "^7.18.6", + "@material-ui/core": "^4.12.4", + "@material-ui/icons": "^4.11.3", "@rails/actioncable": "^6.0.0", "@rails/activestorage": "^6.0.0", "@rails/ujs": "^6.0.0", "@rails/webpacker": "4.3.0", + "axios": "^1.2.1", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "eslint": "^8.29.0", "eslint-config-airbnb": "^19.0.4", @@ -22,9 +25,12 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.31.11", "eslint-plugin-react-hooks": "^4.6.0", + "humps": "^2.0.1", "material-design-lite": "^1.3.0", "prettier": "^2.8.0", "prop-types": "^15.8.1", + "qs": "^6.11.0", + "ramda": "^0.28.0", "react": "^18.2.0", "react-dom": "^18.2.0", "styled-components": "^5.3.6", diff --git a/test/controllers/api/v1/tasks_controller_test.rb b/test/controllers/api/v1/tasks_controller_test.rb index 2197fa0..d4ce848 100644 --- a/test/controllers/api/v1/tasks_controller_test.rb +++ b/test/controllers/api/v1/tasks_controller_test.rb @@ -49,5 +49,4 @@ class Api::V1::TasksControllerTest < ActionController::TestCase assert !Task.where(id: task.id).exists? end - end diff --git a/yarn.lock b/yarn.lock index 2a1d6b8..e447385 100644 --- a/yarn.lock +++ b/yarn.lock @@ -947,7 +947,7 @@ core-js-pure "^3.25.1" regenerator-runtime "^0.13.11" -"@babel/runtime@^7.10.2", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.9", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.2", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.20.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== @@ -993,6 +993,11 @@ resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== +"@emotion/hash@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + "@emotion/is-prop-valid@^1.1.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz#7f2d35c97891669f7e276eb71c83376a5dc44c83" @@ -1099,6 +1104,77 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@material-ui/core@^4.12.4": + version "4.12.4" + resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.12.4.tgz#4ac17488e8fcaf55eb6a7f5efb2a131e10138a73" + integrity sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ== + dependencies: + "@babel/runtime" "^7.4.4" + "@material-ui/styles" "^4.11.5" + "@material-ui/system" "^4.12.2" + "@material-ui/types" "5.1.0" + "@material-ui/utils" "^4.11.3" + "@types/react-transition-group" "^4.2.0" + clsx "^1.0.4" + hoist-non-react-statics "^3.3.2" + popper.js "1.16.1-lts" + prop-types "^15.7.2" + react-is "^16.8.0 || ^17.0.0" + react-transition-group "^4.4.0" + +"@material-ui/icons@^4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.11.3.tgz#b0693709f9b161ce9ccde276a770d968484ecff1" + integrity sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA== + dependencies: + "@babel/runtime" "^7.4.4" + +"@material-ui/styles@^4.11.5": + version "4.11.5" + resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.5.tgz#19f84457df3aafd956ac863dbe156b1d88e2bbfb" + integrity sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA== + dependencies: + "@babel/runtime" "^7.4.4" + "@emotion/hash" "^0.8.0" + "@material-ui/types" "5.1.0" + "@material-ui/utils" "^4.11.3" + clsx "^1.0.4" + csstype "^2.5.2" + hoist-non-react-statics "^3.3.2" + jss "^10.5.1" + jss-plugin-camel-case "^10.5.1" + jss-plugin-default-unit "^10.5.1" + jss-plugin-global "^10.5.1" + jss-plugin-nested "^10.5.1" + jss-plugin-props-sort "^10.5.1" + jss-plugin-rule-value-function "^10.5.1" + jss-plugin-vendor-prefixer "^10.5.1" + prop-types "^15.7.2" + +"@material-ui/system@^4.12.2": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.12.2.tgz#f5c389adf3fce4146edd489bf4082d461d86aa8b" + integrity sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw== + dependencies: + "@babel/runtime" "^7.4.4" + "@material-ui/utils" "^4.11.3" + csstype "^2.5.2" + prop-types "^15.7.2" + +"@material-ui/types@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2" + integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A== + +"@material-ui/utils@^4.11.3": + version "4.11.3" + resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.11.3.tgz#232bd86c4ea81dab714f21edad70b7fdf0253942" + integrity sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg== + dependencies: + "@babel/runtime" "^7.4.4" + prop-types "^15.7.2" + react-is "^16.8.0 || ^17.0.0" + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -1323,6 +1399,13 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" +"@types/react-transition-group@^4.2.0": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" + integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA== + dependencies: + "@types/react" "*" + "@types/react@*": version "18.0.26" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.26.tgz#8ad59fc01fef8eaf5c74f4ea392621749f0b7917" @@ -1880,6 +1963,15 @@ axe-core@^4.4.3: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.5.2.tgz#823fdf491ff717ac3c58a52631d4206930c1d9f7" integrity sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA== +axios@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.1.tgz#44cf04a3c9f0c2252ebd85975361c026cb9f864a" + integrity sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -2491,6 +2583,11 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clsx@^1.0.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + coa@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" @@ -2558,7 +2655,7 @@ colorette@^2.0.10: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2925,6 +3022,14 @@ css-tree@^1.1.2: mdn-data "2.0.14" source-map "^0.6.1" +css-vendor@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" + integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== + dependencies: + "@babel/runtime" "^7.8.3" + is-in-browser "^1.0.2" + css-what@^3.2.1: version "3.4.2" resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" @@ -3020,6 +3125,11 @@ csso@^4.0.2: dependencies: css-tree "^1.1.2" +csstype@^2.5.2: + version "2.6.21" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e" + integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w== + csstype@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" @@ -3205,6 +3315,14 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -4013,7 +4131,7 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -4028,6 +4146,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -4569,6 +4696,16 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +humps@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" + integrity sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g== + +hyphenate-style-name@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" + integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -4909,6 +5046,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-in-browser@^1.0.2, is-in-browser@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" + integrity sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g== + is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" @@ -5170,6 +5312,76 @@ jsprim@^1.2.2: json-schema "0.4.0" verror "1.10.0" +jss-plugin-camel-case@^10.5.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.2.tgz#76dddfa32f9e62d17daa4e3504991fd0933b89e1" + integrity sha512-wgBPlL3WS0WDJ1lPJcgjux/SHnDuu7opmgQKSraKs4z8dCCyYMx9IDPFKBXQ8Q5dVYij1FFV0WdxyhuOOAXuTg== + dependencies: + "@babel/runtime" "^7.3.1" + hyphenate-style-name "^1.0.3" + jss "10.9.2" + +jss-plugin-default-unit@^10.5.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.2.tgz#3e7f4a1506b18d8fe231554fd982439feb2a9c53" + integrity sha512-pYg0QX3bBEFtTnmeSI3l7ad1vtHU42YEEpgW7pmIh+9pkWNWb5dwS/4onSfAaI0kq+dOZHzz4dWe+8vWnanoSg== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.2" + +jss-plugin-global@^10.5.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.9.2.tgz#e7f2ad4a5e8e674fb703b04b57a570b8c3e5c2c2" + integrity sha512-GcX0aE8Ef6AtlasVrafg1DItlL/tWHoC4cGir4r3gegbWwF5ZOBYhx04gurPvWHC8F873aEGqge7C17xpwmp2g== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.2" + +jss-plugin-nested@^10.5.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.9.2.tgz#3aa2502816089ecf3981e1a07c49b276d67dca63" + integrity sha512-VgiOWIC6bvgDaAL97XCxGD0BxOKM0K0zeB/ECyNaVF6FqvdGB9KBBWRdy2STYAss4VVA7i5TbxFZN+WSX1kfQA== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.2" + tiny-warning "^1.0.2" + +jss-plugin-props-sort@^10.5.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.2.tgz#645f6c8f179309667b3e6212f66b59a32fb3f01f" + integrity sha512-AP1AyUTbi2szylgr+O0OB7gkIxEGzySLITZ2GpsaoX72YMCGI2jYAc+WUhPfvUnZYiauF4zTnN4V4TGuvFjJlw== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.2" + +jss-plugin-rule-value-function@^10.5.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.2.tgz#9afe07596e477123cbf11120776be6a64494541f" + integrity sha512-vf5ms8zvLFMub6swbNxvzsurHfUZ5Shy5aJB2gIpY6WNA3uLinEcxYyraQXItRHi5ivXGqYciFDRM2ZoVoRZ4Q== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.2" + tiny-warning "^1.0.2" + +jss-plugin-vendor-prefixer@^10.5.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.2.tgz#410a0f3b9f8dbbfba58f4d329134df4849aa1237" + integrity sha512-SxcEoH+Rttf9fEv6KkiPzLdXRmI6waOTcMkbbEFgdZLDYNIP9UKNHFy6thhbRKqv0XMQZdrEsbDyV464zE/dUA== + dependencies: + "@babel/runtime" "^7.3.1" + css-vendor "^2.0.8" + jss "10.9.2" + +jss@10.9.2, jss@^10.5.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss/-/jss-10.9.2.tgz#9379be1f195ef98011dfd31f9448251bd61b95a9" + integrity sha512-b8G6rWpYLR4teTUbGd4I4EsnWjg7MN0Q5bSsjKhVkJVjhQDy2KzkbD2AW3TuT0RYZVmZZHKIrXDn6kjU14qkUg== + dependencies: + "@babel/runtime" "^7.3.1" + csstype "^3.0.2" + is-in-browser "^1.1.3" + tiny-warning "^1.0.2" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: version "3.3.3" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea" @@ -6378,6 +6590,11 @@ pnp-webpack-plugin@^1.5.0: dependencies: ts-pnp "^1.1.6" +popper.js@1.16.1-lts: + version "1.16.1-lts" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05" + integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA== + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -7060,7 +7277,7 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== -prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -7077,6 +7294,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -7149,7 +7371,7 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== -qs@6.11.0: +qs@6.11.0, qs@^6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== @@ -7189,6 +7411,11 @@ raf-schd@^4.0.2: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== +ramda@^0.28.0: + version "0.28.0" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.28.0.tgz#acd785690100337e8b063cab3470019be427cc97" + integrity sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA== + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -7245,7 +7472,7 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.2: +"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== @@ -7262,6 +7489,16 @@ react-redux@^7.2.0: prop-types "^15.7.2" react-is "^17.0.2" +react-transition-group@^4.4.0: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -8439,6 +8676,11 @@ tiny-invariant@^1.0.6: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== +tiny-warning@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"