diff --git a/app/javascript/packs/components/EditPopup/EditPopup.js b/app/javascript/packs/components/EditPopup/EditPopup.js index b69bf69..a14bbe5 100644 --- a/app/javascript/packs/components/EditPopup/EditPopup.js +++ b/app/javascript/packs/components/EditPopup/EditPopup.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { isNil } from 'ramda'; +import { isNil, has } from 'ramda'; import Button from '@material-ui/core/Button'; import Card from '@material-ui/core/Card'; @@ -14,6 +14,8 @@ import PropTypes from 'prop-types'; import Form from './components/Form'; import TaskPresenter from 'presenters/TaskPresenter'; +import ImageUpload from 'packs/components/ImageUpload'; +import TasksRepository from 'repositories/TasksRepository'; import useStyles from './useStyles'; @@ -27,6 +29,14 @@ function EditPopup({ cardId, onClose, onCardDestroy, onCardLoad, onCardUpdate }) onCardLoad(cardId).then(setTask); }, []); + const onRemoveImage = () => { + TasksRepository.removeImage(task.id); + }; + + const onAttachImage = (attachment) => { + TasksRepository.attachImage(attachment, task.id); + }; + const handleCardUpdate = () => { setSaving(true); @@ -49,6 +59,7 @@ function EditPopup({ cardId, onClose, onCardDestroy, onCardLoad, onCardUpdate }) alert(`Destrucion Failed! Error: ${error.message}`); }); }; + const isLoading = isNil(task); return ( @@ -84,6 +95,20 @@ function EditPopup({ cardId, onClose, onCardDestroy, onCardLoad, onCardUpdate }) > Destroy + {isNil(TaskPresenter.imageUrl(task)) ? ( +
+ +
+ ) : ( +
+ + Attachment + + +
+ )} diff --git a/app/javascript/packs/components/EditPopup/useStyles.js b/app/javascript/packs/components/EditPopup/useStyles.js index 01fdc0b..ca9459f 100644 --- a/app/javascript/packs/components/EditPopup/useStyles.js +++ b/app/javascript/packs/components/EditPopup/useStyles.js @@ -21,6 +21,14 @@ const useStyles = makeStyles(() => ({ display: 'flex', justifyContent: 'flex-end', }, + + imageUploadContainer: { + display: 'flex', + }, + + previewContainer: { + display: 'flex', + }, })); export default useStyles; diff --git a/app/javascript/packs/components/ImageUpload/ImageUpload.js b/app/javascript/packs/components/ImageUpload/ImageUpload.js new file mode 100644 index 0000000..253d605 --- /dev/null +++ b/app/javascript/packs/components/ImageUpload/ImageUpload.js @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { isNil, path } from 'ramda'; +import Button from '@material-ui/core/Button'; +import ReactCrop, { makeAspectCrop } from 'react-image-crop'; +import 'react-image-crop/dist/ReactCrop.css'; + +import useStyles from './useStyles'; + +function ImageUpload({ onUpload }) { + const styles = useStyles(); + + const DEFAULT_CROP_PARAMS = { + units: 'px', + x: 0, + y: 0, + width: 100, + height: 100, + }; + + const [fileAsBase64, changeFileAsBase64] = useState(null); + const [cropParams, changeCropParams] = useState(DEFAULT_CROP_PARAMS); + const [file, changeFile] = useState(null); + const [image, changeImage] = useState(null); + + const handleCropComplete = (newCrop, newPercentageCrop) => { + changeCropParams(newPercentageCrop); + }; + + const onImageLoaded = (loadedImage) => { + const newCropParams = makeAspectCrop(DEFAULT_CROP_PARAMS, loadedImage.width, loadedImage.height); + changeCropParams(newCropParams); + changeImage(loadedImage); + }; + + const getActualCropParameters = (width, height, params) => ({ + cropX: (params.x * width) / 100, + cropY: (params.y * height) / 100, + cropWidth: (params.width * width) / 100, + cropHeight: (params.height * height) / 100, + }); + + const handleCropChange = (_, newCropParams) => { + changeCropParams(newCropParams); + }; + + const handleSave = () => { + const { naturalWidth: width, naturalHeight: height } = image; + const actualCropParams = getActualCropParameters(width, height, cropParams); + onUpload({ attachment: { ...actualCropParams, image: file } }); + }; + + const handleImageRead = (newImage) => changeFileAsBase64(path(['target', 'result'], newImage)); + + const handleLoadFile = (e) => { + e.preventDefault(); + + const [acceptedFile] = e.target.files; + + const fileReader = new FileReader(); + fileReader.onload = handleImageRead; + fileReader.readAsDataURL(acceptedFile); + changeFile(acceptedFile); + }; + + return fileAsBase64 ? ( + <> +
+ +
+ + + ) : ( + + ); +} + +ImageUpload.propTypes = { + onUpload: PropTypes.func.isRequired, +}; + +export default ImageUpload; diff --git a/app/javascript/packs/components/ImageUpload/index.js b/app/javascript/packs/components/ImageUpload/index.js new file mode 100644 index 0000000..b3a99c0 --- /dev/null +++ b/app/javascript/packs/components/ImageUpload/index.js @@ -0,0 +1,3 @@ +import ImageUpload from './ImageUpload'; + +export default ImageUpload; diff --git a/app/javascript/packs/components/ImageUpload/useStyles.js b/app/javascript/packs/components/ImageUpload/useStyles.js new file mode 100644 index 0000000..cb5f6d8 --- /dev/null +++ b/app/javascript/packs/components/ImageUpload/useStyles.js @@ -0,0 +1,13 @@ +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(() => ({ + crop: { + maxHeight: 500, + maxWidth: 500, + position: 'absolute', + top: '35%', + left: '33%', + }, +})); + +export default useStyles; diff --git a/app/javascript/presenters/TaskPresenter.js b/app/javascript/presenters/TaskPresenter.js index 6765c58..1536293 100644 --- a/app/javascript/presenters/TaskPresenter.js +++ b/app/javascript/presenters/TaskPresenter.js @@ -10,6 +10,7 @@ export default new PropTypesPresenter( assignee: PropTypes.object, transitions: PropTypes.array, state: PropTypes.string, + imageUrl: PropTypes.string, }, { title(task) { diff --git a/app/javascript/repositories/TasksRepository.js b/app/javascript/repositories/TasksRepository.js index f4d251a..0f5653f 100644 --- a/app/javascript/repositories/TasksRepository.js +++ b/app/javascript/repositories/TasksRepository.js @@ -26,4 +26,14 @@ export default { const path = routes.apiV1TaskPath(id); return FetchHelper.delete(path); }, + + attachImage(attachment, id) { + const path = routes.attachImageApiV1TaskPath(id); + return FetchHelper.putFormData(path, attachment); + }, + + removeImage(id) { + const path = routes.removeImageApiV1TaskPath(id); + return FetchHelper.delete(path); + }, }; diff --git a/app/javascript/routes/ApiRoutes.js b/app/javascript/routes/ApiRoutes.js index 6358236..12cb024 100644 --- a/app/javascript/routes/ApiRoutes.js +++ b/app/javascript/routes/ApiRoutes.js @@ -3,500 +3,500 @@ * 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; + 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; + } } - 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, + 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), }; - } - 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); + } + 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; + 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; @@ -511,7 +511,7 @@ export const serialize = __jsr.serialize; * @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"]]]]]]]]]); +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 @@ -519,7 +519,7 @@ export const adminUserPath = __jsr.r({ "id": { "r": true }, "format": {} }, [2, * @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"]]]]]]]); +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 @@ -528,7 +528,7 @@ export const adminUsersPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, "a * @param {object | undefined} options * @returns {string} route path */ -export const apiV1TaskPath = __jsr.r({ "id": { "r": true }, "format": { "d": "json" } }, [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"]]]]]]]]]]]); +export const apiV1TaskPath = __jsr.r({"id":{"r":true},"format":{"d":"json"}}, [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 @@ -536,7 +536,7 @@ export const apiV1TaskPath = __jsr.r({ "id": { "r": true }, "format": { "d": "js * @param {object | undefined} options * @returns {string} route path */ -export const apiV1TasksPath = __jsr.r({ "format": { "d": "json" } }, [2, [7, "/"], [2, [6, "api"], [2, [7, "/"], [2, [6, "v1"], [2, [7, "/"], [2, [6, "tasks"], [1, [2, [8, "."], [3, "format"]]]]]]]]]); +export const apiV1TasksPath = __jsr.r({"format":{"d":"json"}}, [2,[7,"/"],[2,[6,"api"],[2,[7,"/"],[2,[6,"v1"],[2,[7,"/"],[2,[6,"tasks"],[1,[2,[8,"."],[3,"format"]]]]]]]]]); /** * Generates rails route to @@ -545,7 +545,7 @@ export const apiV1TasksPath = __jsr.r({ "format": { "d": "json" } }, [2, [7, "/" * @param {object | undefined} options * @returns {string} route path */ -export const apiV1UserPath = __jsr.r({ "id": { "r": true }, "format": {} }, [2, [7, "/"], [2, [6, "api"], [2, [7, "/"], [2, [6, "v1"], [2, [7, "/"], [2, [6, "users"], [2, [7, "/"], [2, [3, "id"], [1, [2, [8, "."], [3, "format"]]]]]]]]]]]); +export const apiV1UserPath = __jsr.r({"id":{"r":true},"format":{"d":"json"}}, [2,[7,"/"],[2,[6,"api"],[2,[7,"/"],[2,[6,"v1"],[2,[7,"/"],[2,[6,"users"],[2,[7,"/"],[2,[3,"id"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]); /** * Generates rails route to @@ -553,7 +553,16 @@ export const apiV1UserPath = __jsr.r({ "id": { "r": true }, "format": {} }, [2, * @param {object | undefined} options * @returns {string} route path */ -export const apiV1UsersPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, "api"], [2, [7, "/"], [2, [6, "v1"], [2, [7, "/"], [2, [6, "users"], [1, [2, [8, "."], [3, "format"]]]]]]]]]); +export const apiV1UsersPath = __jsr.r({"format":{"d":"json"}}, [2,[7,"/"],[2,[6,"api"],[2,[7,"/"],[2,[6,"v1"],[2,[7,"/"],[2,[6,"users"],[1,[2,[8,"."],[3,"format"]]]]]]]]]); + +/** + * Generates rails route to + * /api/v1/tasks/:id/attach_image(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const attachImageApiV1TaskPath = __jsr.r({"id":{"r":true},"format":{"d":"json"}}, [2,[7,"/"],[2,[6,"api"],[2,[7,"/"],[2,[6,"v1"],[2,[7,"/"],[2,[6,"tasks"],[2,[7,"/"],[2,[3,"id"],[2,[7,"/"],[2,[6,"attach_image"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]); /** * Generates rails route to @@ -561,7 +570,7 @@ export const apiV1UsersPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, "a * @param {object | undefined} options * @returns {string} route path */ -export const boardPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, "board"], [1, [2, [8, "."], [3, "format"]]]]]); +export const boardPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"board"],[1,[2,[8,"."],[3,"format"]]]]]); /** * Generates rails route to @@ -569,7 +578,7 @@ export const boardPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, "board" * @param {object | undefined} options * @returns {string} route path */ -export const developersPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, "developers"], [1, [2, [8, "."], [3, "format"]]]]]); +export const developersPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"developers"],[1,[2,[8,"."],[3,"format"]]]]]); /** * Generates rails route to @@ -578,7 +587,15 @@ export const developersPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, "d * @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"]]]]]]]]]]]); +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 + * /password_resets/edit(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const editPasswordResetsPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"password_resets"],[2,[7,"/"],[2,[6,"edit"],[1,[2,[8,"."],[3,"format"]]]]]]]); /** * Generates rails route to @@ -587,7 +604,49 @@ export const editAdminUserPath = __jsr.r({ "id": { "r": true }, "format": {} }, * @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"]]]]]]]]]]]]]]]); +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 + * /letter_opener + * @param {object | undefined} options + * @returns {string} route path + */ +export const letterOpenerWebPath = __jsr.r({}, [2,[7,"/"],[6,"letter_opener"]]); + +/** + * Generates rails route to + * /letter_opener/ + * @param {object | undefined} options + * @returns {string} route path + */ +export const letterOpenerWebLettersPath = __jsr.r({}, [2,[2,[7,"/"],[6,"letter_opener"]],[7,"/"]]); + +/** + * Generates rails route to + * /letter_opener/clear(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const letterOpenerWebClearLettersPath = __jsr.r({"format":{}}, [2,[2,[2,[7,"/"],[6,"letter_opener"]],[7,"/"]],[2,[6,"clear"],[1,[2,[8,"."],[3,"format"]]]]]); + +/** + * Generates rails route to + * /letter_opener/:id(/:style)(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const letterOpenerWebLetterPath = __jsr.r({"id":{"r":true},"style":{},"format":{}}, [2,[2,[2,[7,"/"],[6,"letter_opener"]],[7,"/"]],[2,[3,"id"],[2,[1,[2,[7,"/"],[3,"style"]]],[1,[2,[8,"."],[3,"format"]]]]]]); + +/** + * Generates rails route to + * /letter_opener/:id/delete(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const letterOpenerWebDeleteLetterPath = __jsr.r({"id":{"r":true},"format":{}}, [2,[2,[2,[7,"/"],[6,"letter_opener"]],[7,"/"]],[2,[3,"id"],[2,[7,"/"],[2,[6,"delete"],[1,[2,[8,"."],[3,"format"]]]]]]]); /** * Generates rails route to @@ -595,7 +654,7 @@ export const editRailsConductorInboundEmailPath = __jsr.r({ "id": { "r": true }, * @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"]]]]]]]]]); +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 @@ -603,7 +662,15 @@ export const newAdminUserPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, * @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"]]]]]]]); +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 + * /password_resets/new(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const newPasswordResetsPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"password_resets"],[2,[7,"/"],[2,[6,"new"],[1,[2,[8,"."],[3,"format"]]]]]]]); /** * Generates rails route to @@ -611,7 +678,7 @@ export const newDeveloperPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, * @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"]]]]]]]]]]]]]); +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 @@ -619,7 +686,15 @@ export const newRailsConductorInboundEmailPath = __jsr.r({ "format": {} }, [2, [ * @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"]]]]]]]); +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 + * /password_resets(.:format) + * @param {object | undefined} options + * @returns {string} route path + */ +export const passwordResetsPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"password_resets"],[1,[2,[8,"."],[3,"format"]]]]]); /** * Generates rails route to @@ -630,7 +705,7 @@ export const newSessionPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, "s * @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"]]]]]]]]]]]]]]]); +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 @@ -639,7 +714,7 @@ export const railsBlobRepresentationPath = __jsr.r({ "signed_blob_id": { "r": tr * @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"]]]]]]]]]]]]]); +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 @@ -648,7 +723,7 @@ export const railsConductorInboundEmailPath = __jsr.r({ "id": { "r": true }, "fo * @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"]]]]]]]]]]]]]); +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 @@ -656,7 +731,7 @@ export const railsConductorInboundEmailReroutePath = __jsr.r({ "inbound_email_id * @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"]]]]]]]]]]]); +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 @@ -664,7 +739,7 @@ export const railsConductorInboundEmailsPath = __jsr.r({ "format": {} }, [2, [7, * @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"]]]]]]]]]); +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 @@ -674,7 +749,7 @@ export const railsDirectUploadsPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2 * @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"]]]]]]]]]]]]]); +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 @@ -682,7 +757,7 @@ export const railsDiskServicePath = __jsr.r({ "encoded_key": { "r": true }, "fil * @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"]]]]]]]); +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 @@ -690,7 +765,7 @@ export const railsInfoPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, "ra * @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"]]]]]]]]]); +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 @@ -698,7 +773,7 @@ export const railsInfoPropertiesPath = __jsr.r({ "format": {} }, [2, [7, "/"], [ * @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"]]]]]]]]]); +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 @@ -706,7 +781,7 @@ export const railsInfoRoutesPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [ * @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"]]]]]]]); +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 @@ -714,7 +789,7 @@ export const railsMailersPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, * @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"]]]]]]]]]]]]]); +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 @@ -722,7 +797,7 @@ export const railsMailgunInboundEmailsPath = __jsr.r({ "format": {} }, [2, [7, " * @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"]]]]]]]]]]]); +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 @@ -730,7 +805,7 @@ export const railsMandrillInboundEmailsPath = __jsr.r({ "format": {} }, [2, [7, * @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"]]]]]]]]]]]); +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 @@ -738,7 +813,7 @@ export const railsMandrillInboundHealthCheckPath = __jsr.r({ "format": {} }, [2, * @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"]]]]]]]]]]]); +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 @@ -746,7 +821,7 @@ export const railsPostmarkInboundEmailsPath = __jsr.r({ "format": {} }, [2, [7, * @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"]]]]]]]]]]]); +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 @@ -754,7 +829,7 @@ export const railsRelayInboundEmailsPath = __jsr.r({ "format": {} }, [2, [7, "/" * @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"]]]]]]]]]]]); +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 @@ -764,7 +839,16 @@ export const railsSendgridInboundEmailsPath = __jsr.r({ "format": {} }, [2, [7, * @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"]]]]]]]]]]]]]); +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 + * /api/v1/tasks/:id/remove_image(.:format) + * @param {any} id + * @param {object | undefined} options + * @returns {string} route path + */ +export const removeImageApiV1TaskPath = __jsr.r({"id":{"r":true},"format":{"d":"json"}}, [2,[7,"/"],[2,[6,"api"],[2,[7,"/"],[2,[6,"v1"],[2,[7,"/"],[2,[6,"tasks"],[2,[7,"/"],[2,[3,"id"],[2,[7,"/"],[2,[6,"remove_image"],[1,[2,[8,"."],[3,"format"]]]]]]]]]]]]]); /** * Generates rails route to @@ -772,7 +856,7 @@ export const railsServiceBlobPath = __jsr.r({ "signed_id": { "r": true }, "filen * @param {object | undefined} options * @returns {string} route path */ -export const rootPath = __jsr.r({}, [7, "/"]); +export const rootPath = __jsr.r({}, [7,"/"]); /** * Generates rails route to @@ -780,7 +864,15 @@ export const rootPath = __jsr.r({}, [7, "/"]); * @param {object | undefined} options * @returns {string} route path */ -export const sessionPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, "session"], [1, [2, [8, "."], [3, "format"]]]]]); +export const sessionPath = __jsr.r({"format":{}}, [2,[7,"/"],[2,[6,"session"],[1,[2,[8,"."],[3,"format"]]]]]); + +/** + * Generates rails route to + * /admin/sidekiq + * @param {object | undefined} options + * @returns {string} route path + */ +export const sidekiqWebPath = __jsr.r({}, [2,[7,"/"],[2,[6,"admin"],[2,[7,"/"],[6,"sidekiq"]]]]); /** * Generates rails route to @@ -789,5 +881,5 @@ export const sessionPath = __jsr.r({ "format": {} }, [2, [7, "/"], [2, [6, "sess * @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"]]]]]]]]]]]); +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/utils/fetchHelper.js b/app/javascript/utils/fetchHelper.js index 537f9b0..baa6334 100644 --- a/app/javascript/utils/fetchHelper.js +++ b/app/javascript/utils/fetchHelper.js @@ -1,5 +1,6 @@ import axios from 'axios'; import qs from 'qs'; +import { objectToFormData } from 'object-to-formdata'; import { camelize, decamelize } from './keysConverter'; @@ -64,4 +65,18 @@ export default { return axios.delete(url, body).then(camelize); }, + + putFormData(url, json) { + const body = decamelize(json); + body.attachment.image = json.attachment.image; + const formData = objectToFormData(body); + + return axios + .put(url, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + .then(camelize); + }, }; diff --git a/app/services/attachment_service.rb b/app/services/attachments_service.rb similarity index 100% rename from app/services/attachment_service.rb rename to app/services/attachments_service.rb diff --git a/config/environments/development.rb b/config/environments/development.rb index ce46256..09b7254 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -60,6 +60,7 @@ # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. + routes.default_url_options[:host] = 'localhost:3330' config.file_watcher = ActiveSupport::EventedFileUpdateChecker config.hosts << '.app.uffizzi.com' end diff --git a/package.json b/package.json index c63ffb3..192ba14 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,14 @@ "eslint-plugin-react-hooks": "^4.6.0", "humps": "^2.0.1", "material-design-lite": "^1.3.0", + "object-to-formdata": "3.0.9", "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", + "react-image-crop": "8.6.4", "react-redux": "^8.0.5", "react-select": "^5.7.0", "redux": "^4.2.0", @@ -44,4 +46,4 @@ "devDependencies": { "webpack-dev-server": "^4.11.1" } -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index df6f8f2..56738d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2929,6 +2929,11 @@ core-js@^3.4.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.26.1.tgz#7a9816dabd9ee846c1c0fe0e8fcad68f3709134e" integrity sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA== +core-js@^3.4.2: + version "3.27.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.27.1.tgz#23cc909b315a6bb4e418bf40a52758af2103ba46" + integrity sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww== + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -6305,6 +6310,11 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== +object-to-formdata@3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/object-to-formdata/-/object-to-formdata-3.0.9.tgz#40e3314522345789259738811c0222b7d6e556e9" + integrity sha512-1JDHRFSpk6wzhPBAAqdVncdvbjZ+bjB0tioruNdKn8UyudBBXiBxRa7PJyvYqp4ioEKX98dEOX9Fw1RxA+vLzg== + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -7613,6 +7623,15 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-image-crop@8.6.4: + version "8.6.4" + resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-8.6.4.tgz#51289c22baf7d116575102e885f879af71376388" + integrity sha512-5buyhUg9slzW+QGvN2dbpGSI1VqPxxmfEwe19TZXUB7LjW5+ljZOnrTSsteIzeBqmz6ngVucc8l+OrwgcDOSNg== + dependencies: + clsx "^1.0.4" + core-js "^3.4.2" + prop-types "^15.7.2" + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"