From 42fbacdd6606ecf4427b36ff68a0c0a503bf43de Mon Sep 17 00:00:00 2001 From: muerwre Date: Wed, 7 Aug 2019 14:23:04 +0700 Subject: [PATCH] fixed eslint --- .eslintrc.js | 133 ++++++------------- package-lock.json | 113 ++++++++++++++++ package.json | 4 + src/components/editors/EditorPanel/index.tsx | 7 +- src/redux/types.ts | 5 + src/redux/uploads/constants.ts | 1 + src/redux/uploads/sagas.ts | 94 ++++++++++++- src/utils/uploader.ts | 33 +++++ src/utils/validators.ts | 39 ++++++ 9 files changed, 327 insertions(+), 102 deletions(-) create mode 100644 src/utils/uploader.ts create mode 100644 src/utils/validators.ts diff --git a/.eslintrc.js b/.eslintrc.js index 4b23f3e5..4da03f7e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,48 +1,6 @@ -// module.exports = { -// extends: ['airbnb', 'airbnb-base', 'plugin:@typescript-eslint/recommended'], -// // "parser": "babel-eslint", -// parser: '@typescript-eslint/parser', -// parserOptions: { -// ecmaFeatures: { -// jsx: true -// }, -// project: './tsconfig.json' -// }, -// plugins: ['@typescript-eslint', 'react', 'jsx-a11y', 'import', 'react-hooks'], -// rules: { -// indent: ['error', 2], -// '@typescript-eslint/indent': ['error', 2], -// 'comma-dangle': 0, -// 'no-restricted-syntax': 1, -// 'new-cap': 1, -// 'no-continue': 1, -// 'no-underscore-dangle': 1, -// 'global-require': 1, -// 'react/no-multi-comp': 1, -// 'react/jsx-filename-extension': 0, -// camelcase: 1, -// 'import/no-unresolved': 1, -// 'import/prefer-default-export': 1, -// 'import/extensions': 1, -// 'no-return-assign': 1, -// 'max-len': 1, -// 'jsx-a11y/no-static-element-interactions': 0, -// 'jsx-a11y/click-events-have-key-events': 0, -// 'jsx-a11y/interactive-supports-focus': 0, -// 'react-hooks/rules-of-hooks': 'error', -// 'react-hooks/exhaustive-deps': 'warn', -// 'no-nested-ternary': 1 -// }, -// globals: { -// document: false, -// window: false, -// HTMLInputElement: false, -// HTMLDivElement: false -// } -// }; module.exports = { - extends: ['plugin:@typescript-eslint/recommended'], - plugins: ['import', '@typescript-eslint'], + extends: ['airbnb', 'airbnb-base', 'plugin:@typescript-eslint/recommended'], + // "parser": "babel-eslint", parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { @@ -50,58 +8,45 @@ module.exports = { }, project: './tsconfig.json', }, - globals: { - Reactotron: true, + plugins: ['@typescript-eslint', 'react', 'jsx-a11y', 'import', 'react-hooks'], + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, }, rules: { - curly: ['error', 'all'], - 'valid-jsdoc': 'error', - 'linebreak-style': 'off', - 'no-console': 'off', - 'object-curly-newline': 'off', - 'no-unused-expressions': 'off', - 'no-unused-vars': 'off', - 'prefer-destructuring': [ - 'error', - { - VariableDeclarator: { - object: true, - }, - }, - ], - 'function-paren-newline': ['error', 'consistent'], - 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], - 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], - - 'eslint-comments/no-unlimited-disable': 'off', - - 'import/no-unresolved': 'off', - 'import/extensions': 'off', - 'import/prefer-default-export': 'off', - - 'prettier/prettier': [ - 'error', - { - singleQuote: true, - parser: 'flow', - trailingComma: 'all', - printWidth: 100, - }, - '@format', - ], - + indent: ['error', 2], + '@typescript-eslint/explicit-function-return-type': 0, '@typescript-eslint/indent': ['error', 2], - '@typescript-eslint/explicit-function-return-type': [ - 'off', - { - allowExpressions: true, - allowTypedFunctionExpressions: true, - allowHigherOrderFunctions: true, - }, - ], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/explicit-member-accessibility': 'off', - '@typescript-eslint/no-empty-interface': 'off', + 'comma-dangle': 0, + 'no-restricted-syntax': 1, + 'new-cap': 1, + 'no-continue': 1, + 'no-underscore-dangle': 1, + 'global-require': 1, + 'react/no-multi-comp': 1, + 'react/jsx-filename-extension': 0, + '@typescript-eslint/camelcase': 0, + '@typescript-eslint/interface-name-prefix': 0, + camelcase: 0, + 'import/no-unresolved': 1, + 'import/prefer-default-export': 1, + 'import/extensions': 1, + 'no-return-assign': 1, + 'max-len': 1, + 'jsx-a11y/no-static-element-interactions': 0, + 'jsx-a11y/click-events-have-key-events': 0, + 'jsx-a11y/interactive-supports-focus': 0, + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + 'no-nested-ternary': 1, + }, + globals: { + document: false, + window: false, + HTMLInputElement: false, + HTMLDivElement: false, }, }; diff --git a/package-lock.json b/package-lock.json index fb053ae9..a9989d96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2609,6 +2609,12 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==" }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/node": { "version": "11.11.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.1.tgz", @@ -6475,6 +6481,11 @@ "assert-plus": "^1.0.0" } }, + "date-fns": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==" + }, "date-now": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", @@ -6513,6 +6524,12 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "dev": true + }, "default-gateway": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", @@ -7228,6 +7245,34 @@ "resolve": "^1.5.0" } }, + "eslint-import-resolver-typescript": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-1.1.1.tgz", + "integrity": "sha512-jqSfumQ+H5y3FUJ6NjRkbOQSUOlbBucGTN3ELymOtcDBbPjVdm/luvJuCfCaIXGh8sEF26ma1qVdtDgl9ndhUg==", + "dev": true, + "requires": { + "debug": "^4.0.1", + "resolve": "^1.4.0", + "tsconfig-paths": "^3.6.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "eslint-import-resolver-webpack": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.9.0.tgz", @@ -7500,6 +7545,11 @@ } } }, + "eslint-plugin-react-hooks": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.6.1.tgz", + "integrity": "sha512-wHhmGJyVuijnYIJXZJHDUF2WM+rJYTjulUTqF9k61d3BTk8etydz+M4dXUVH7M76ZRS85rqBTCx0Es/lLsrjnA==" + }, "eslint-rule-composer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", @@ -16504,6 +16554,36 @@ } } }, + "tsconfig-paths": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.8.0.tgz", + "integrity": "sha512-zZEYFo4sjORK8W58ENkRn9s+HmQFkkwydDG7My5s/fnfr2YYCaiyXe/HBUcIgU8epEKOXwiahOO+KZYjiXlWyQ==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "deepmerge": "^2.0.1", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -16763,6 +16843,39 @@ "integrity": "sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==", "dev": true }, + "typescript-eslint-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/typescript-eslint-parser/-/typescript-eslint-parser-22.0.0.tgz", + "integrity": "sha512-pD8D7oTeRwWvFVxK3PaY6FYAiZsuRXFkIc2+1xkwCT3NduySgCgjeAkR5/dnIWecOiFVcEHf4ypXurF02Q6Z3Q==", + "dev": true, + "requires": { + "eslint-scope": "^4.0.0", + "eslint-visitor-keys": "^1.0.0", + "typescript-estree": "18.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + } + } + }, + "typescript-estree": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/typescript-estree/-/typescript-estree-18.0.0.tgz", + "integrity": "sha512-HxTWrzFyYOPWA91Ij7xL9mNUVpGTKLH2KiaBn28CMbYgX2zgWdJqU9hO7Are+pAPAqY91NxAYoaAyDDZ3rLj2A==", + "dev": true, + "requires": { + "lodash.unescape": "4.0.1", + "semver": "5.5.0" + } + }, "ua-parser-js": { "version": "0.7.17", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", diff --git a/package.json b/package.json index 9c80cfcd..1b314873 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "eslint": "^5.16.0", "eslint-config-airbnb": "^17.1.1", "eslint-import-resolver-babel-module": "^4.0.0", + "eslint-import-resolver-typescript": "^1.1.1", "eslint-import-resolver-webpack": "^0.9.0", "eslint-loader": "^2.2.1", "eslint-plugin-babel": "^5.3.0", @@ -47,6 +48,7 @@ "style-loader": "^0.21.0", "ts-node": "^8.0.1", "typescript": "^3.2.4", + "typescript-eslint-parser": "^22.0.0", "uglifyjs-webpack-plugin": "^1.3.0", "webpack": "^4.6.0", "webpack-cli": "^3.2.3", @@ -65,8 +67,10 @@ "classnames": "^2.2.6", "clean-webpack-plugin": "^0.1.9", "connected-react-router": "^6.3.2", + "date-fns": "^1.30.1", "dotenv": "^8.0.0", "dotenv-webpack": "^1.7.0", + "eslint-plugin-react-hooks": "^1.6.1", "history": "^4.7.2", "http-errors": "~1.6.2", "less": "^3.8.1", diff --git a/src/components/editors/EditorPanel/index.tsx b/src/components/editors/EditorPanel/index.tsx index ca82721b..fb513b8a 100644 --- a/src/components/editors/EditorPanel/index.tsx +++ b/src/components/editors/EditorPanel/index.tsx @@ -6,11 +6,8 @@ interface IProps { data: INode; setData: (val: INode) => void; onUpload: (val: File[]) => void; -}; +} -const EditorPanel: FC = ({ -}) => ( -
- ); +const EditorPanel: FC = ({}) =>
; export { EditorPanel }; diff --git a/src/redux/types.ts b/src/redux/types.ts index 355a9de9..506ee7a8 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -64,6 +64,11 @@ export interface IFile { updatedAt?: string; } +export interface IFileWithUUID { + temp_id?: UUID; + file: File; +} + export interface IBlock { type: 'image' | 'text' | 'media' | 'youtube' | 'video', temp_ids: UUID[]; diff --git a/src/redux/uploads/constants.ts b/src/redux/uploads/constants.ts index 231ebdf8..d620d6ee 100644 --- a/src/redux/uploads/constants.ts +++ b/src/redux/uploads/constants.ts @@ -4,6 +4,7 @@ const prefix = 'UPLOAD.'; export const UPLOAD_ACTIONS = { UPLOAD_FILES: `${prefix}UPLOAD_FILES`, + UPLOAD_CANCEL: `${prefix}UPLOAD_CANCEL`, }; export const EMPTY_FILE: IFile = { diff --git a/src/redux/uploads/sagas.ts b/src/redux/uploads/sagas.ts index a3ec41e5..c95461eb 100644 --- a/src/redux/uploads/sagas.ts +++ b/src/redux/uploads/sagas.ts @@ -1,6 +1,94 @@ -import {takeEvery} from "redux-saga/effects"; -import {UPLOAD_ACTIONS} from "~/redux/uploads/constants"; +import { takeEvery, all, spawn, call, put, take, fork, race } from 'redux-saga/effects'; +import { UPLOAD_ACTIONS } from '~/redux/uploads/constants'; +import { uploadUploadFiles } from './actions'; +import { reqWrapper } from '../auth/sagas'; +import { createUploader, uploadGetThumb } from '~/utils/uploader'; +import { HTTP_RESPONSES } from '~/utils/api'; +import { VALIDATORS } from '~/utils/validators'; +import { UUID, IFileWithUUID, IResultWithStatus } from '../types'; + +function* uploadCall({ temp_id, onProgress, file }) { + return yield call(reqWrapper, console.log, { file, onProgress }); +} + +function* onUploadProgress(chan) { + while (true) { + const { progress, temp_id }: { progress: number; temp_id: string } = yield take(chan); + console.log('progress', { progress, temp_id }); + // replace with the one, that changes upload status with progress + // yield put(inventoryUploadSet(temp_id, { progress })); + } +} + +function* uploadCancelWorker(id) { + while (true) { + const { temp_id } = yield take(UPLOAD_ACTIONS.UPLOAD_CANCEL); + if (temp_id === id) break; + } + + return true; +} + +function* uploadWorker(file: File, temp_id: UUID) { + const [promise, chan] = createUploader<{ temp_id; file }, { temp_id }>(uploadCall, { temp_id }); + yield fork(onUploadProgress, chan); + + return yield call(promise, { temp_id, file }); +} +function* uploadFile({ file, temp_id }: IFileWithUUID): IResultWithStatus { + if (!file.type || !VALIDATORS.IS_IMAGE_MIME(file.type)) { + return { error: 'File_Not_Image', status: HTTP_RESPONSES.BAD_REQUEST, data: {} }; + } + + const preview = yield call(uploadGetThumb, file); + + // yield put(inventoryUploadAdd( // replace with the one, what adds file upload status + // temp_id, + // { + // ...EMPTY_INVENTORY_UPLOAD, + // preview, + // is_uploading: true, + // type: file.type, + // }, + // )); + + const { result, cancel, cancel_editing, save_inventory } = yield race({ + result: call(uploadWorker, file, temp_id), + cancel: call(uploadCancelWorker, temp_id), + // add here CANCEL_UPLOADS worker, that will watch for subject + // cancel_editing: take(UPLOAD_ACTIONS.CANCEL_EDITING), + // save_inventory: take(INVENTORY_ACTIONS.SAVE_INVENTORY), + }) as any; + + if (cancel || cancel_editing || save_inventory) { + // return yield put(inventoryUploadDrop(temp_id)); // replace with the one, that will delete file upload status record + return { error: null, status: HTTP_RESPONSES.NOT_FOUND, data: {} }; + } + + const { data, error } = result; + + if (error) { + // replace with the one, that changes file upload status to error + // return yield put(inventoryUploadSet(temp_id, { is_uploading: false, error: data.detail || error })); + return { error: null, status: HTTP_RESPONSES.NOT_FOUND, data: {} }; + } + + // replace with the one, that updates upload status with actual data + // yield put(inventoryUploadSet(temp_id, { + // is_uploading: false, + // error: null, + // uuid: data.uuid, + // url: data.url, + // thumbnail_url: data.url, + // })); + + return { error: null, status: HTTP_RESPONSES.CREATED, data: {} }; // add file here as data +} + +function* uploadFiles({ files }: ReturnType) { + yield all(files.map(file => spawn(uploadFile, file))); +} export default function* () { - yield takeEvery(UPLOAD_ACTIONS.UPLOAD_FILES, console.log); + yield takeEvery(UPLOAD_ACTIONS.UPLOAD_FILES, uploadFiles); } diff --git a/src/utils/uploader.ts b/src/utils/uploader.ts new file mode 100644 index 00000000..403f57f8 --- /dev/null +++ b/src/utils/uploader.ts @@ -0,0 +1,33 @@ +import { eventChannel, END } from 'redux-saga'; +import { VALIDATORS } from '~/utils/validators'; + +export const IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg']; + +export function createUploader + (callback: (args: any) => any, payload: R): + [(args: T) => (args: T & { onProgress: (current: number, total: number) => void }) => any, EventChannel] { + let emit; + + const chan = eventChannel(emitter => { + emit = emitter; + return () => null; + }); + + const onProgress = (current: number, total: number): void => { + emit(current >= total ? END : { ...payload, progress: parseFloat((current / total).toFixed(1)) }); + }; + + const wrappedCallback = args => callback({ ...args, onProgress }); + + return [wrappedCallback, chan]; +} + +export const uploadGetThumb = async file => { + if (!file.type || !VALIDATORS.IS_IMAGE_MIME(file.type)) return ''; + + return await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result || ''); + reader.readAsDataURL(file); + }); +}; diff --git a/src/utils/validators.ts b/src/utils/validators.ts new file mode 100644 index 00000000..bbbc9052 --- /dev/null +++ b/src/utils/validators.ts @@ -0,0 +1,39 @@ +import { IMAGE_MIME_TYPES } from '~/utils/uploader'; +import isValid from 'date-fns/isValid'; +import { IAddress } from '~/redux/types'; + +const isValidEmail = (email: string): boolean => + !!email && + String(email) && + !!String(email).match( + /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, + ); + +const isLikeEmail = (email: string): boolean => + !!email && String(email) && !!String(email).match(/^([^\@]+)@([^\@]+)\.([^\@]+)$$/); + +const isNonEmpty = (value: string): boolean => !!value && value.trim().length > 0; +const isLikePhone = isNonEmpty; + +const isAtLeast = (length: number, value: string): boolean => + !!value && value.trim().length >= length; + +const isMimeOfImage = (mime): boolean => !!mime && IMAGE_MIME_TYPES.indexOf(mime) >= 0; + +const isDate = (val: string): boolean => !!val && isValid(new Date(val)); +const isStringDate = (val: string): boolean => !!val && !!val.match(/^[\d]{2,4}\-[\d]{2}-[\d]{2}/); +const isAddrWithRaw = ({ raw }: Partial): boolean => !!raw && isNonEmpty(raw); + +export const VALIDATORS = { + EMAIL: isValidEmail, + LIKE_PHONE: isLikePhone, + LIKE_EMAIL: isLikeEmail, + NON_EMPTY: isNonEmpty, + AT_LEAST: length => isAtLeast.bind(null, length), + IS_IMAGE_MIME: isMimeOfImage, + IS_DATE: isDate, + IS_STRINGY_DATE: isStringDate, + IS_ADDRESS_WITH_RAW: isAddrWithRaw, + EVOLVE: (validator: (val: any) => boolean, error: string) => (val: any) => + !val || !validator(val) ? error : null, +};