├── .gitignore ├── LICENSE.txt ├── README.md ├── README_RU.md ├── frontend └── foc_csv │ ├── .babelrc │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .postcssrc.js │ ├── README.md │ ├── build │ ├── build.js │ ├── check-versions.js │ ├── logo.png │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js │ ├── config │ ├── dev.env.js │ ├── index.js │ └── prod.env.js │ ├── index.html │ ├── package.json │ ├── src │ ├── App.vue │ ├── api │ │ ├── index.js │ │ ├── modules │ │ │ ├── exporter.js │ │ │ ├── importer.js │ │ │ └── index.js │ │ └── routes.js │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── common │ │ │ ├── ErrorMessage.vue │ │ │ ├── Navigation.vue │ │ │ ├── ProgressBar.vue │ │ │ └── attributeWidgets │ │ │ │ ├── TextAttribute.vue │ │ │ │ ├── TextareaAttribute.vue │ │ │ │ └── index.js │ │ ├── exporter │ │ │ ├── AttributesEncoder.vue │ │ │ ├── Export.vue │ │ │ ├── ExportFields.vue │ │ │ ├── ImagesExportSettings.vue │ │ │ ├── LeftSidebar.vue │ │ │ ├── RightSidebar.vue │ │ │ └── attributeWidgets │ │ │ │ └── index.js │ │ ├── importer │ │ │ ├── AdditionalProcessingSettings.vue │ │ │ ├── AttributesParser.vue │ │ │ ├── CsvColumnNameBinder.vue │ │ │ ├── CsvFileUpload.vue │ │ │ ├── CsvToDbMatcher.vue │ │ │ ├── DbFieldsSelect.vue │ │ │ ├── ImagesImportSettings.vue │ │ │ ├── ImagesZipUpload.vue │ │ │ ├── Import.vue │ │ │ ├── LeftSidebar.vue │ │ │ ├── LineSkipSettings.vue │ │ │ ├── MultiCsvFieldsSelector.vue │ │ │ ├── MulticolumnExtractor.vue │ │ │ ├── RightSidebar.vue │ │ │ ├── StatusRewrites.vue │ │ │ ├── StatusRewritesItem.vue │ │ │ └── attributeWidgets │ │ │ │ ├── ColumnAttribute.vue │ │ │ │ ├── DBColumnAttribute.vue │ │ │ │ └── index.js │ │ ├── info │ │ │ └── Info.vue │ │ └── util │ │ │ ├── BackupRestore.vue │ │ │ ├── ExportProfileData.vue │ │ │ ├── ImportProfileData.vue │ │ │ ├── ProfilesControlList.vue │ │ │ ├── RestoreProfile.vue │ │ │ ├── RestoreProfiles.vue │ │ │ └── SerializedDataToggler.vue │ ├── config.js │ ├── helpers.js │ ├── i18n.js │ ├── i18n │ │ ├── en.json │ │ └── ru.json │ ├── main.js │ ├── mixins │ │ └── util.js │ ├── router │ │ └── index.js │ ├── store │ │ ├── common │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ └── mutations.js │ │ ├── exporter │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ ├── mutation-types.js │ │ │ ├── mutations.js │ │ │ └── state.js │ │ ├── importer │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ ├── mutation-types.js │ │ │ ├── mutations.js │ │ │ └── state.js │ │ └── index.js │ └── test.json │ ├── static │ └── .gitkeep │ └── yarn.lock ├── gulpfile.js ├── install.xml ├── package.json ├── upload ├── admin │ ├── controller │ │ └── extension │ │ │ └── module │ │ │ └── foc_csv.php │ ├── language │ │ ├── en-gb │ │ │ └── extension │ │ │ │ └── module │ │ │ │ ├── foc_attribute_encoders.php │ │ │ │ ├── foc_attribute_parsers.php │ │ │ │ └── foc_csv.php │ │ └── ru-ru │ │ │ └── extension │ │ │ └── module │ │ │ ├── foc_attribute_encoders.php │ │ │ ├── foc_attribute_parsers.php │ │ │ └── foc_csv.php │ ├── model │ │ └── extension │ │ │ └── module │ │ │ ├── foc_csv.php │ │ │ ├── foc_csv_common.php │ │ │ └── foc_csv_exporter.php │ └── view │ │ ├── javascript │ │ └── .gitignore │ │ └── template │ │ └── extension │ │ └── module │ │ ├── foc_csv.tpl │ │ └── foc_csv.twig └── system │ └── library │ └── FocSimpleTemplater.php └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | node_modules 3 | yarn-error.log 4 | .DS_Store 5 | compiled-files -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FO_CSV - flexible CSV import/export for Opencart 2 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FFreeocart%2Ffo-csv.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FFreeocart%2Ffo-csv?ref=badge_shield) 3 | 4 | 5 | With this module, you can simplify import/export data from CSV files in your Opencart project. 6 | Current version developed and tested on Opencart 2.3/3.0.2.0. 7 | 8 | ### Why do we need another import/export module? 9 | 10 | There is known fact that Opencart licensed under free GPLv3 license, but many extensions uses they own license (proprietary in most cases) - many authors limiting access to source code (especially in Russia). 11 | 12 | On the one hand, authors try to protect they code with encrypting software (ionCube, etc) and force users to accept their license (for example, license keys check). 13 | 14 | As a user, you cannot modify encrypted source code by yourself. Authors not always can modify their products for your case, so you got in vendor lock. 15 | 16 | Also, enctypted software cannot give you any security guarantees, because you dont know what code do in these encrypted sections. 17 | 18 | Project [Freeocart](http://freeocart.ru) aims to create opensource ecosystem for Opencart. 19 | 20 | ### UNSTABLE VERSION 21 | 22 | At the moment extension is under heavy development, some functions may work incorrect. 23 | 24 | I'm open to any help - ISSUES/PR 25 | 26 | ### Attention! 27 | 28 | Use this software at your own risk! 29 | 30 | Author or contributors are not responsible at data corruption/remove caused by using this software. 31 | 32 | We are strongly recommend you to backup data before using this. 33 | 34 | Source code distributed "as is", all parts of the software is free and opensource and respecting GPLv3 license. 35 | 36 | ## License 37 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FFreeocart%2Ffo-csv.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FFreeocart%2Ffo-csv?ref=badge_large) -------------------------------------------------------------------------------- /README_RU.md: -------------------------------------------------------------------------------- 1 | # FO_CSV - гибкий импорт/экспорт данных из CSV для Opencart 2 | 3 | Модуль для Opencart, призванный упростить процедуру импортирования данных в онлайн магазин на Opencart. 4 | 5 | Текущая версия модуля разработана и протестирована под версию Opencart 2.3/3.0.2.0. 6 | 7 | ### Зачем ещё один модуль импорта/экспорта 8 | 9 | Как известно, Opencart распространяется под свободной лицензией GPLv3, однако на многие модули и дополнения это не распространяется - многие авторы модулей ограничивают возможность просмотра/модификации исходных кодов своего ПО. 10 | 11 | С одной стороны, авторы пытаются защитить свой код использованием разного шифрующего ПО (например, ionCube) и заставить пользователей соблюдать лицензионное соглашение. 12 | 13 | Однако, такое отношение к распространению сильно ограничивает свободы пользователя - он не сможет доработать код под свою задачу (авторы модулей неохотно/дорого дорабатывают свои модули), пользователь не может знать что происходит внутри зашифрованного участка кода, а потому автор не может гарантировать пользователю безопасность такого ПО, также пользователь не может поспособствовать улучшению качества ПО. 14 | 15 | Проект [Freeocart](http://freeocart.ru) нацелен на разработку свободной и открытой экосистемы для Opencart. 16 | 17 | ### Нестабильная версия 18 | 19 | На данный момент модуль находится в активной разработке, поэтому некоторые функции ещё не реализованы/могут работать некорректно. 20 | 21 | Приветствуется любая помощь - ISSUES/PR 22 | 23 | ### Внимание! 24 | 25 | Используйте данное ПО на свой страх и риск! 26 | 27 | Ни автор, ни контрибьюторы проекта не отвечают за порчу/удаление данных, возникшие при использовании этого ПО. 28 | 29 | Код распространяется "как есть", все части ПО являются открытым программным обеспечением, распространяемым согласно лицензии GPLv3. -------------------------------------------------------------------------------- /frontend/foc_csv/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /frontend/foc_csv/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /frontend/foc_csv/.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | -------------------------------------------------------------------------------- /frontend/foc_csv/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 28 | "brace-style": [2, "stroustrup", { "allowSingleLine": true }] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/foc_csv/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /frontend/foc_csv/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/foc_csv/README.md: -------------------------------------------------------------------------------- 1 | # foc_csv 2 | 3 | > foc_csv frontend 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | ``` 20 | 21 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 22 | -------------------------------------------------------------------------------- /frontend/foc_csv/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /frontend/foc_csv/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /frontend/foc_csv/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freeocart/fo-csv/6dcd8eaa1b125c873f9637ba93fbfd634622e02e/frontend/foc_csv/build/logo.png -------------------------------------------------------------------------------- /frontend/foc_csv/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /frontend/foc_csv/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/foc_csv/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | const createLintingRule = () => ({ 12 | test: /\.(js|vue)$/, 13 | loader: 'eslint-loader', 14 | enforce: 'pre', 15 | include: [resolve('src'), resolve('test')], 16 | options: { 17 | formatter: require('eslint-friendly-formatter'), 18 | emitWarning: !config.dev.showEslintErrorsInOverlay 19 | } 20 | }) 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './src/main.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | 'vue$': 'vue/dist/vue.esm.js', 38 | '@': resolve('src'), 39 | } 40 | }, 41 | module: { 42 | rules: [ 43 | ...(config.dev.useEslint ? [createLintingRule()] : []), 44 | { 45 | test: /\.vue$/, 46 | loader: 'vue-loader', 47 | options: vueLoaderConfig 48 | }, 49 | { 50 | test: /\.js$/, 51 | loader: 'babel-loader', 52 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 53 | }, 54 | { 55 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 56 | loader: 'url-loader', 57 | options: { 58 | limit: 10000, 59 | name: utils.assetsPath('img/[name].[ext]') 60 | } 61 | }, 62 | { 63 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 64 | loader: 'url-loader', 65 | options: { 66 | limit: 10000, 67 | name: utils.assetsPath('media/[name].[ext]') 68 | } 69 | }, 70 | { 71 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 72 | loader: 'url-loader', 73 | options: { 74 | limit: 10000, 75 | name: utils.assetsPath('fonts/[name].[ext]') 76 | } 77 | } 78 | ] 79 | }, 80 | node: { 81 | // prevent webpack from injecting useless setImmediate polyfill because Vue 82 | // source contains it (although only uses it if it's native). 83 | setImmediate: false, 84 | // prevent webpack from injecting mocks to Node native modules 85 | // that does not make sense for the client 86 | dgram: 'empty', 87 | fs: 'empty', 88 | net: 'empty', 89 | tls: 'empty', 90 | child_process: 'empty' 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /frontend/foc_csv/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /frontend/foc_csv/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = require('../config/prod.env') 15 | 16 | const webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true, 21 | usePostCSS: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].js'), 28 | chunkFilename: utils.assetsPath('js/[id].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new UglifyJsPlugin({ 36 | uglifyOptions: { 37 | compress: { 38 | warnings: false 39 | } 40 | }, 41 | sourceMap: config.build.productionSourceMap, 42 | parallel: true 43 | }), 44 | // extract css into its own file 45 | new ExtractTextPlugin({ 46 | filename: utils.assetsPath('css/[name].css'), 47 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 51 | allChunks: true, 52 | }), 53 | // Compress extracted CSS. We are using this plugin so that possible 54 | // duplicated CSS from different components can be deduped. 55 | new OptimizeCSSPlugin({ 56 | cssProcessorOptions: config.build.productionSourceMap 57 | ? { safe: true, map: { inline: false } } 58 | : { safe: true } 59 | }), 60 | // generate dist index.html with correct asset hash for caching. 61 | // you can customize output by editing /index.html 62 | // see https://github.com/ampedandwired/html-webpack-plugin 63 | new HtmlWebpackPlugin({ 64 | filename: config.build.index, 65 | template: 'index.html', 66 | inject: false, 67 | minify: { 68 | removeComments: true, 69 | collapseWhitespace: true, 70 | removeAttributeQuotes: false 71 | // more options: 72 | // https://github.com/kangax/html-minifier#options-quick-reference 73 | }, 74 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 75 | chunksSortMode: 'dependency' 76 | }), 77 | // keep module.id stable when vendor modules does not change 78 | new webpack.HashedModuleIdsPlugin(), 79 | // enable scope hoisting 80 | new webpack.optimize.ModuleConcatenationPlugin(), 81 | // split vendor js into its own file 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'vendor', 84 | minChunks (module) { 85 | // any required modules inside node_modules are extracted to vendor 86 | return ( 87 | module.resource && 88 | /\.js$/.test(module.resource) && 89 | module.resource.indexOf( 90 | path.join(__dirname, '../node_modules') 91 | ) === 0 92 | ) 93 | } 94 | }), 95 | // extract webpack runtime and module manifest to its own file in order to 96 | // prevent vendor hash from being updated whenever app bundle is updated 97 | new webpack.optimize.CommonsChunkPlugin({ 98 | name: 'manifest', 99 | minChunks: Infinity 100 | }), 101 | // This instance extracts shared chunks from code splitted chunks and bundles them 102 | // in a separate chunk, similar to the vendor chunk 103 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 104 | new webpack.optimize.CommonsChunkPlugin({ 105 | name: 'app', 106 | async: 'vendor-async', 107 | children: true, 108 | minChunks: 3 109 | }), 110 | 111 | // copy custom static assets 112 | new CopyWebpackPlugin([ 113 | { 114 | from: path.resolve(__dirname, '../static'), 115 | to: config.build.assetsSubDirectory, 116 | ignore: ['.*'] 117 | } 118 | ]) 119 | ] 120 | }) 121 | 122 | if (config.build.productionGzip) { 123 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 124 | 125 | webpackConfig.plugins.push( 126 | new CompressionWebpackPlugin({ 127 | asset: '[path].gz[query]', 128 | algorithm: 'gzip', 129 | test: new RegExp( 130 | '\\.(' + 131 | config.build.productionGzipExtensions.join('|') + 132 | ')$' 133 | ), 134 | threshold: 10240, 135 | minRatio: 0.8 136 | }) 137 | ) 138 | } 139 | 140 | if (config.build.bundleAnalyzerReport) { 141 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 142 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 143 | } 144 | 145 | module.exports = webpackConfig 146 | -------------------------------------------------------------------------------- /frontend/foc_csv/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/foc_csv/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | 14 | proxyTable: { 15 | '/': { 16 | target: process.env.PROXY_URL || 'http://oc2.local/', 17 | changeOrigin: true 18 | } 19 | }, 20 | 21 | // Various Dev Server settings 22 | host: 'localhost', // can be overwritten by process.env.HOST 23 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 24 | autoOpenBrowser: true, 25 | errorOverlay: true, 26 | notifyOnErrors: true, 27 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 28 | 29 | // Use Eslint Loader? 30 | // If true, your code will be linted during bundling and 31 | // linting errors and warnings will be shown in the console. 32 | useEslint: true, 33 | // If true, eslint errors and warnings will also be shown in the error overlay 34 | // in the browser. 35 | showEslintErrorsInOverlay: false, 36 | 37 | /** 38 | * Source Maps 39 | */ 40 | 41 | // https://webpack.js.org/configuration/devtool/#development 42 | devtool: 'cheap-module-eval-source-map', 43 | 44 | // If you have problems debugging vue-files in devtools, 45 | // set this to false - it *may* help 46 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 47 | cacheBusting: true, 48 | 49 | cssSourceMap: true 50 | }, 51 | 52 | build: { 53 | // Template for index.html 54 | index: path.resolve(__dirname, '../dist/index.html'), 55 | 56 | // Paths 57 | assetsRoot: path.resolve(__dirname, '../../../upload/admin/view/javascript/foc_csv'), 58 | assetsSubDirectory: '', 59 | assetsPublicPath: '../../../upload/admin/view/javascript/foc_csv', 60 | 61 | /** 62 | * Source Maps 63 | */ 64 | 65 | productionSourceMap: true, 66 | // https://webpack.js.org/configuration/devtool/#production 67 | devtool: '#source-map', 68 | 69 | // Gzip off by default as many popular static hosts such as 70 | // Surge or Netlify already gzip all static assets for you. 71 | // Before setting to `true`, make sure to: 72 | // npm install --save-dev compression-webpack-plugin 73 | productionGzip: false, 74 | productionGzipExtensions: ['js', 'css'], 75 | 76 | // Run the build command with an extra argument to 77 | // View the bundle analyzer report after build finishes: 78 | // `npm run build --report` 79 | // Set to `true` or `false` to always turn it on or off 80 | bundleAnalyzerReport: process.env.npm_config_report 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /frontend/foc_csv/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /frontend/foc_csv/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | foc_csv 7 | 8 | 9 | 10 | 11 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /frontend/foc_csv/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foc_csv", 3 | "version": "1.0.0", 4 | "description": "foc_csv frontend", 5 | "author": "ikenfin ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "yarn run dev", 10 | "lint": "eslint --ext .js,.vue src", 11 | "build": "node build/build.js" 12 | }, 13 | "dependencies": { 14 | "autocomplete-vue": "^1.1.0", 15 | "papaparse": "^5.3.0", 16 | "vue": "^2.6.12", 17 | "vue-i18n": "^8.22.4", 18 | "vue-resource": "1.5.3", 19 | "vue-router": "^3.5.1", 20 | "vuex": "^3.6.2", 21 | "vuex-models": "^1.0.5" 22 | }, 23 | "devDependencies": { 24 | "autoprefixer": "^7.1.2", 25 | "babel-core": "^6.22.1", 26 | "babel-eslint": "^8.2.1", 27 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 28 | "babel-loader": "^7.1.1", 29 | "babel-plugin-syntax-jsx": "^6.18.0", 30 | "babel-plugin-transform-runtime": "^6.22.0", 31 | "babel-plugin-transform-vue-jsx": "^3.5.0", 32 | "babel-preset-env": "^1.3.2", 33 | "babel-preset-stage-2": "^6.22.0", 34 | "chalk": "^2.0.1", 35 | "copy-webpack-plugin": "^4.0.1", 36 | "css-loader": "^0.28.0", 37 | "eslint": "^4.19.1", 38 | "eslint-config-standard": "^10.2.1", 39 | "eslint-friendly-formatter": "^3.0.0", 40 | "eslint-loader": "^1.7.1", 41 | "eslint-plugin-import": "^2.22.1", 42 | "eslint-plugin-node": "^5.2.0", 43 | "eslint-plugin-promise": "^3.4.0", 44 | "eslint-plugin-standard": "^3.0.1", 45 | "eslint-plugin-vue": "^4.0.0", 46 | "extract-text-webpack-plugin": "^3.0.0", 47 | "file-loader": "^1.1.4", 48 | "friendly-errors-webpack-plugin": "^1.6.1", 49 | "html-webpack-plugin": "^2.30.1", 50 | "node-notifier": "^5.1.2", 51 | "optimize-css-assets-webpack-plugin": "^3.2.0", 52 | "ora": "^1.2.0", 53 | "portfinder": "^1.0.28", 54 | "postcss-import": "^11.0.0", 55 | "postcss-loader": "^2.0.8", 56 | "postcss-url": "^7.2.1", 57 | "rimraf": "^2.6.0", 58 | "semver": "^5.3.0", 59 | "shelljs": "^0.7.6", 60 | "uglifyjs-webpack-plugin": "^1.1.1", 61 | "url-loader": "^0.5.8", 62 | "vue-loader": "^13.3.0", 63 | "vue-style-loader": "^3.0.1", 64 | "vue-template-compiler": "^2.6.12", 65 | "webpack": "^3.6.0", 66 | "webpack-bundle-analyzer": "^3.3.2", 67 | "webpack-dev-server": "^3.11.2", 68 | "webpack-merge": "^4.1.0" 69 | }, 70 | "engines": { 71 | "node": ">= 6.0.0", 72 | "npm": ">= 3.0.0" 73 | }, 74 | "browserslist": [ 75 | "> 1%", 76 | "last 2 versions", 77 | "not ie <= 8" 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/api/index.js: -------------------------------------------------------------------------------- 1 | import modules from './modules' 2 | /* 3 | ApiProvider 4 | vue plugin to simplify api calling 5 | 6 | it creates routes like this: 7 | Vue.$api.exporter.loadProfile() 8 | in the handler you have additional argument - mkUrl - module specific version of mkUrl: 9 | // exporter module: 10 | async saveProfile (mkUrl, options) { 11 | mkUrl('test') // --> 'test?__DEFAULT_PARAMS__&type=exporter 12 | } 13 | 14 | You can use default version of mkUrl by Vue.$api.mkUrl || this.$api.mkUrl (in components code) 15 | */ 16 | class ApiProvider { 17 | constructor (modules) { 18 | this.modules = modules 19 | this._prepared = false 20 | } 21 | 22 | prepare (options) { 23 | if (!this._prepared) { 24 | this._config = options 25 | 26 | let proxied = { 27 | mkUrl: this.mkUrl.bind(this) 28 | } 29 | 30 | let modules = this.modules 31 | 32 | Object.keys(modules).forEach(type => { 33 | proxied[type] = {} 34 | 35 | Object.keys(modules[type]).forEach(method => { 36 | let moduleCallback = this._typedMkUrl(type) 37 | proxied[type][method] = (options) => modules[type][method](moduleCallback, options) 38 | }) 39 | }) 40 | 41 | this.proxy = proxied 42 | this._prepared = true 43 | 44 | return this 45 | } 46 | else { 47 | throw new Error('Trying to prepare already prepared $api!') 48 | } 49 | } 50 | 51 | install (Vue, options) { 52 | if (!this._prepared) { 53 | this.prepare(options) 54 | } 55 | 56 | Vue.$api = this.proxy 57 | 58 | Vue.mixin({ 59 | created () { 60 | this.$api = Vue.$api 61 | } 62 | }) 63 | } 64 | 65 | _typedMkUrl (type) { 66 | return (action, params) => { 67 | return this.mkUrl(action, { ...params, type }) 68 | } 69 | } 70 | 71 | mkUrl (action, getParams = {}) { 72 | let getString = Object.keys(getParams) 73 | .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(getParams[key])) 74 | .join('&') 75 | 76 | if (getString.length > 0) { 77 | getString = `&${getString}` 78 | } 79 | 80 | return `${this._config.baseUrl}${this._config.baseRoute}/${action}&${this._config.tokenName}=${this._config.token}${getString}` 81 | } 82 | } 83 | 84 | const provider = new ApiProvider(modules) 85 | export default provider 86 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/api/modules/exporter.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { 3 | EXPORT_URL, 4 | SAVE_PROFILE_URL, 5 | SAVE_ALL_PROFILES_URL 6 | } from '@/api/routes' 7 | 8 | export default { 9 | async saveProfile (mkUrl, options) { 10 | return Vue.http.post(mkUrl(SAVE_PROFILE_URL), options) 11 | }, 12 | 13 | async saveProfiles (mkUrl, profiles) { 14 | return Vue.http.post(mkUrl(SAVE_ALL_PROFILES_URL), { 15 | profiles 16 | }) 17 | }, 18 | 19 | async submitData (mkUrl, profile) { 20 | let request = new FormData() 21 | request.append('profile-json', JSON.stringify(profile)) 22 | 23 | return Vue.http.post(mkUrl(EXPORT_URL), request, { 24 | headers: { 25 | 'Content-Type': 'multipart/form-data' 26 | } 27 | }) 28 | }, 29 | 30 | async submitPart (_mkUrl, { callbackUrl, options }) { 31 | return Vue.http.post(callbackUrl, options) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/api/modules/importer.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import { 4 | IMPORT_URL, 5 | SAVE_PROFILE_URL, 6 | SAVE_ALL_PROFILES_URL 7 | } from '@/api/routes' 8 | 9 | export default { 10 | 11 | async saveProfile (mkUrl, options) { 12 | return Vue.http.post(mkUrl(SAVE_PROFILE_URL), options) 13 | }, 14 | 15 | async saveProfiles (mkUrl, profiles) { 16 | return Vue.http.post(mkUrl(SAVE_ALL_PROFILES_URL), { 17 | profiles 18 | }) 19 | }, 20 | 21 | async submitData (mkUrl, { data, profile }) { 22 | let request = new FormData() 23 | request.append('csv-file', data.csvFileRef.files[0]) 24 | 25 | if (data.imagesZipFileRef && data.imagesZipFileRef.files.length > 0) { 26 | request.append('images-zip', data.imagesZipFileRef.files[0]) 27 | } 28 | 29 | request.append('profile-json', JSON.stringify(profile)) 30 | 31 | return Vue.http.post(mkUrl(IMPORT_URL), request, { 32 | headers: { 33 | 'Content-Type': 'multipart/form-data' 34 | } 35 | }) 36 | }, 37 | 38 | async submitPart (_mkUrl, { callbackUrl, options }) { 39 | return Vue.http.post(callbackUrl, options) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/api/modules/index.js: -------------------------------------------------------------------------------- 1 | import importer from './importer' 2 | import exporter from './exporter' 3 | 4 | export default { 5 | importer, 6 | exporter 7 | } 8 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/api/routes.js: -------------------------------------------------------------------------------- 1 | /* 2 | Backend routes 3 | please keep in mind, that baseUrl provided by Application config (main.js, requestConfig) 4 | */ 5 | 6 | const SAVE_PROFILE_URL = 'saveProfile' 7 | const SAVE_ALL_PROFILES_URL = 'saveProfiles' 8 | const IMPORT_URL = 'import' 9 | const EXPORT_URL = 'export' 10 | const ATTRIBUTES_GROUP_AUTOCOMPLETE_URL = 'attributesGroupAutocomplete' 11 | 12 | export { 13 | SAVE_PROFILE_URL, 14 | SAVE_ALL_PROFILES_URL, 15 | IMPORT_URL, 16 | EXPORT_URL, 17 | ATTRIBUTES_GROUP_AUTOCOMPLETE_URL 18 | } 19 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freeocart/fo-csv/6dcd8eaa1b125c873f9637ba93fbfd634622e02e/frontend/foc_csv/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/common/ErrorMessage.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/common/Navigation.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/common/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | 31 | 62 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/common/attributeWidgets/TextAttribute.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/common/attributeWidgets/TextareaAttribute.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 15 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/common/attributeWidgets/index.js: -------------------------------------------------------------------------------- 1 | import TextAttribute from './TextAttribute' 2 | import TextareaAttribute from './TextareaAttribute' 3 | 4 | export default { 5 | TextAttribute, 6 | TextareaAttribute 7 | } 8 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/exporter/AttributesEncoder.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 77 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/exporter/Export.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 200 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/exporter/ExportFields.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 100 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/exporter/ImagesExportSettings.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 45 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/exporter/LeftSidebar.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 104 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/exporter/RightSidebar.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 74 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/exporter/attributeWidgets/index.js: -------------------------------------------------------------------------------- 1 | import CommonWidgets from '../../common/attributeWidgets' 2 | 3 | export default { 4 | ...CommonWidgets 5 | } 6 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/AdditionalProcessingSettings.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/AttributesParser.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 117 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/CsvColumnNameBinder.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 62 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/CsvFileUpload.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 106 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/CsvToDbMatcher.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 73 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/DbFieldsSelect.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 42 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/ImagesImportSettings.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 62 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/ImagesZipUpload.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/Import.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 262 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/LeftSidebar.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 108 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/LineSkipSettings.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 72 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/MultiCsvFieldsSelector.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 45 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/MulticolumnExtractor.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 124 | 125 | 136 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/RightSidebar.vue: -------------------------------------------------------------------------------- 1 | 110 | 111 | 156 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/StatusRewrites.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 43 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/StatusRewritesItem.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 40 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/attributeWidgets/ColumnAttribute.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/attributeWidgets/DBColumnAttribute.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/importer/attributeWidgets/index.js: -------------------------------------------------------------------------------- 1 | import ColumnAttribute from './ColumnAttribute' 2 | import DBColumnAttribute from './DBColumnAttribute' 3 | 4 | import CommonWidgets from '../../common/attributeWidgets' 5 | 6 | export default { 7 | ColumnAttribute, 8 | DBColumnAttribute, 9 | ...CommonWidgets 10 | } 11 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/info/Info.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 77 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/util/BackupRestore.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/util/ExportProfileData.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 73 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/util/ImportProfileData.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 78 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/util/ProfilesControlList.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 39 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/util/RestoreProfile.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/util/RestoreProfiles.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 41 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/components/util/SerializedDataToggler.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Application config 3 | */ 4 | const DEFAULT_PROFILE_NAME = 'default' 5 | 6 | export { 7 | DEFAULT_PROFILE_NAME 8 | } 9 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/helpers.js: -------------------------------------------------------------------------------- 1 | import papaparse from 'papaparse' 2 | 3 | /* 4 | Validate required fields in app config 5 | */ 6 | const validateAppConfig = (config) => { 7 | const requiredFields = ['token', 'baseRoute', 'baseUrl'] 8 | 9 | const keys = Object.keys(config) 10 | const values = Object.values(config) 11 | 12 | if (requiredFields.every(key => keys.includes(key)) && 13 | values.every(value => !!value)) { 14 | return true 15 | } 16 | 17 | return false 18 | } 19 | 20 | /* 21 | Read first string from blob 22 | */ 23 | class FirstLineReader { 24 | constructor () { 25 | this.events = {} 26 | 27 | this.chunkSize = 256 28 | this.readPos = 0 29 | this.reader = new FileReader() 30 | this.lines = [] 31 | this.chunk = '' 32 | this.file = null 33 | this.readedLines = 0 34 | this.skippedBytes = 0 35 | 36 | this.reader.onload = () => { 37 | // it seems that broken symbols automaticaly converts to \uFFFD 38 | // so we just check line endings and try to read again 39 | // one byte more 40 | if (/\uFFFD$/.test(this.reader.result)) { 41 | this.skippedBytes++ 42 | this.step() 43 | } 44 | else { 45 | this.readPos += this.chunkSize + this.skippedBytes 46 | this.skippedBytes = 0 47 | this.chunk += this.reader.result 48 | this.process() 49 | } 50 | } 51 | } 52 | 53 | on (event, cb) { 54 | this.events[event] = cb 55 | } 56 | 57 | _emit (event, args) { 58 | if (typeof this.events[event] === 'function') { 59 | this.events[event].apply(this, args) 60 | } 61 | } 62 | 63 | process () { 64 | /* 65 | Firstly check if file have line breaks at all 66 | Unix: \n 67 | Windows: \r\n 68 | Old macs: \r 69 | */ 70 | if (/(\r\n|\r|\n)/.test(this.chunk)) { 71 | const lines = this.chunk.split(/(\r\n|\r|\n)/) 72 | const line = lines.shift() 73 | 74 | this.readedLines++ 75 | 76 | if (this.readedLines === this.skipLines) { 77 | this._emit('line', [line]) 78 | } 79 | else { 80 | this.chunk = lines.join('\n') 81 | this.step() 82 | } 83 | } 84 | else { 85 | if (this.readPos < this.file.size) { 86 | this.step() 87 | } 88 | else { 89 | this._emit('error', [ 90 | `Seems that file is empty or has invalid newline characters. Please make sure to convert file into valid format.` 91 | ]) 92 | } 93 | } 94 | } 95 | 96 | read (file, encoding, skipLines = 1) { 97 | this.file = file 98 | this.lines = [] 99 | this.chunk = '' 100 | this.readPos = 0 101 | this.encoding = encoding || 'UTF8' 102 | this.skipLines = parseInt(skipLines) 103 | 104 | // minimum 1 105 | if (this.skipLines <= 0) { 106 | this.skipLines = 1 107 | } 108 | 109 | if (isNaN(this.skipLines)) { 110 | this._emit('error', [ 111 | 'Please make sure if skip lines option is number!' 112 | ]) 113 | } 114 | else { 115 | this.step() 116 | } 117 | } 118 | 119 | step () { 120 | let blob = this.file.slice( 121 | this.readPos, 122 | this.readPos + this.chunkSize + this.skippedBytes 123 | ) 124 | 125 | this.reader.readAsText(blob, this.encoding) 126 | } 127 | } 128 | 129 | /* 130 | Valid mimetypes for csv 131 | https://stackoverflow.com/questions/7076042/what-mime-type-should-i-use-for-csv#answer-42140178 132 | */ 133 | const VALID_CSV_MIMETYPES = [ 134 | 'text/csv', 135 | 'text/x-csv', 136 | 'text/comma-separated-values', 137 | 'text/x-comma-separated-values', 138 | 'text/tab-separated-values', 139 | 'application/vnd.ms-excel', 140 | 'application/csv', 141 | 'application/x-csv' 142 | ] 143 | 144 | /* 145 | Simple csv mime-type or filename check 146 | */ 147 | const isFileSeemsLikeCsv = (file) => { 148 | const validMime = VALID_CSV_MIMETYPES.includes(file.type) 149 | const validExt = /^.*\.csv$/.test(file.name) 150 | return validMime || (file.type === '' && validExt) || false 151 | } 152 | 153 | /* 154 | Parse csv headers - stupid split function:) 155 | */ 156 | const parseCsvHeaders = (raw, delimiter = ';') => { 157 | const result = papaparse.parse(raw, { delimiter }) 158 | return result.data.length > 0 ? result.data[0] : [] 159 | } 160 | 161 | /* 162 | Validate profile 163 | for now - just check keyField exists 164 | */ 165 | const validateProfile = (profile) => { 166 | // validate key field 167 | return profile.keyField && profile.bindings[profile.keyField] != null 168 | } 169 | 170 | export { 171 | validateAppConfig, 172 | FirstLineReader, 173 | parseCsvHeaders, 174 | validateProfile, 175 | isFileSeemsLikeCsv 176 | } 177 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | Vue.use(VueI18n) 5 | 6 | export default (locale) => { 7 | return new VueI18n({ 8 | locale, 9 | messages: { 10 | en: require('./i18n/en.json'), 11 | ru: require('./i18n/ru.json') 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Module {name} targets to be a maximum flexible for import/export CSV data, without any restrictions in CSV format.": "Module {name} targets to be a maximum flexible for import/export CSV data, without any restrictions in CSV format.", 3 | "Thank you for choosing {name} opencart module!": "Thank you for choosing {name} opencart module!", 4 | "focsv-support@freeocart.ru": "support@freeocart.ru", 5 | "Please make sure if skip lines option is number!": "Please make sure that headers line number option is valid number!", 6 | "Please, look for instructions below": "For additional information, please visit project website - freeocart.ru" 7 | } 8 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/i18n/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "Not selected": "Не выбрано", 3 | "Import submodule": "Импорт из CSV", 4 | "Export submodule": "Экспорт в CSV", 5 | "Import": "Импорт", 6 | "Export": "Экспорт", 7 | "Util": "Утилиты", 8 | "Mode": "Режим", 9 | "Import utils": "Утилиты импортера", 10 | "Export utils": "Утилиты экспортера", 11 | "Add": "Добавить", 12 | "Delete": "Удалить", 13 | "Unavailable": "Недоступно", 14 | "soon": "скоро", 15 | "Show/hide": "Показать/скрыть", 16 | "Import in progress": "Импорт данных в процессе", 17 | "Export in progress": "Экспорт данных в процессе", 18 | "Start import!": "Начинаем импорт!", 19 | "Start export!": "Начинаем экспорт!", 20 | "Main settings": "Основные настройки", 21 | "Profile": "Профиль", 22 | "Save profile as": "Сохранить профиль как", 23 | "Save profile": "Сохранить профиль как", 24 | "Store": "Магазин", 25 | "Language": "Язык", 26 | "Encoding": "Кодировка", 27 | "Images settings": "Настройки изображений", 28 | "Images ZIP file": "ZIP Архив картинок", 29 | "Images list delimiter": "Разделитель в поле изображений", 30 | "If no preview - set it from gallery": "Если нет превью - установить из галереи", 31 | "Clear gallery before import": "Удалить изображения из галереи перед импортом", 32 | "Download images with URL": "Подкачивать картинки с URL", 33 | "Images import mode": "Режим установки изображений", 34 | "Add images": "Добавить загруженные", 35 | "Skip if gallery has images": "Не добавлять если галерея не пуста", 36 | "Fields settings": "Управление полями", 37 | "CSV file": "CSV файл", 38 | "Process lines per query": "Сколько обновлять записей за раз", 39 | "CSV headers control": "Управление заголовками CSV", 40 | "CSV without headers": "CSV без заголовков", 41 | "CSV headers line number": "Номер строки с заголовками", 42 | "Skip lines": "Пропустить строки", 43 | "Key field": "Ключевое поле (по нему идет сличение)", 44 | "Fields matching": "Сопоставление полей", 45 | "CSV field": "CSV поле", 46 | "DB field": "DB поле", 47 | "Field delimiter": "Разделитель полей", 48 | "Clear manufacturers before import": "Удалить производителей перед импортом", 49 | "Import mode": "Режим импорта", 50 | "Only update existing": "Только обновить существующие", 51 | "Force add all as new": "Добавить все как новые", 52 | "Update existing and add new": "Обновить существующие и добавить новые", 53 | "Only add missing as new": "Только добавить отсутствующие", 54 | "Remove all matched": "Удалить совпавшие", 55 | "Remove all unmatched": "Удалить несовпавшие", 56 | "Category level delimiter": "Разделитель вложенности категорий", 57 | "Categories delimiter": "Разделитель категорий", 58 | "Fill parent categories": "Заполнить родительские категории", 59 | "Remove chars from category fields": "Удалить символы из поля категорий", 60 | "Opencart status": "Статус в Opencart", 61 | "CSV status": "Статус в CSV", 62 | "Processing settings": "Настройки обработки", 63 | "Replace existing attributes": "Заменить существующие атрибуты", 64 | "Default attributes group": "Группа по умолчанию для атрибутов", 65 | "Attributes import": "Импорт атрибутов", 66 | "Attributes field": "Поле для извлечения атрибутов", 67 | "Attributes parser": "Парсер атрибутов", 68 | 69 | "Backup utils": "Утилиты бэкапирования", 70 | "Restore utils": "Утилиты восстановления", 71 | 72 | "There is errors while doing job!": "Во время выполнения возникли ошибки!", 73 | 74 | "Show current profile data": "Показать данные текущего профиля", 75 | "Show profiles data": "Показать данные всех профилей", 76 | "Show all data state": "Показать состояние", 77 | 78 | "Restore profiles": "Восстановить профили", 79 | "Restore state to profile": "Восстановить в профиль", 80 | "Profile name": "Название профиля", 81 | "Restore": "Восстановить", 82 | "Are you sure? This will remove all profiles before trying to add new ones!": "Вы уверены? Это действие удалит все существующие профили перед тем как добавить новые!", 83 | 84 | "Remove utils": "Утилиты удаления", 85 | "Profile control": "Управление профилями", 86 | "Are you sure you want remove this item?": "Вы уверены что хотите удалить этот элемент?", 87 | "Add new column binding": "Добавить новую привязку к столбцу", 88 | "Attribute name": "Название атрибута", 89 | "Attribute value": "Значение атрибута", 90 | "Skip line if fields empty": "Пропустить строку если поля пустые", 91 | "Default status": "Статус по умолчанию", 92 | "Default stock status": "Складской статус", 93 | 94 | "We catched some errors during import, please check foc logs!": "Во время импорта произошли ошибки, пожалуйста, проверьте логи foc!", 95 | "During export we catched some errors, please check foc logs!": "Во время экспорта произошли ошибки, пожалуйста, проверьте логи foc!", 96 | "Errors count": "Количество ошибок", 97 | 98 | "Error during sending!": "Ошибка во время отправки данных!", 99 | "Invalid profile!": "Ошибки в профиле!", 100 | "Import canceled!": "Импорт отменён!", 101 | "Re-read CSV": "Перечитать CSV", 102 | "Something wrong with your file! Please choose another.": "С выбранным файлом что-то не так! Пожалуйста, попробуйте другой.", 103 | "Seems that file is empty or has invalid newline characters. Please make sure to convert file into valid format.": "Похоже что вы пытаетесь использовать файл с неподдерживаемыми разделителями строк. Пожалуйста, сконвертируйте файл корректно", 104 | "Please make sure if skip lines option is number!": "Пожалуйста, убедитесь что параметр Номер строки с заголовками является числом!", 105 | "Error while reading file": "Ошибка чтения файла", 106 | "Wrong file format": "Некорректный формат файла.", 107 | 108 | "Attention! You chosed a danger import mode!": "Внимание! Вы выбрали опасный режим импорта!", 109 | "Before import from CSV, we will remove all product related data from your database!": "Перед импортом данных из CSV, будут удалены все данные о продуктах из вашей базы данных!", 110 | "Please, make sure you have working backup before you continue!": "Пожалуйста, убедитесь что у вас есть рабочий бэкап перед тем как продолжать!", 111 | "Are you totally sure you want do this???! With this import method you can lose your data!": "Вы абсолютно уверены, что хотите продолжить???! Этот режим импорта может привести к потере всех ваших данных!", 112 | 113 | "Complete": "Завершено", 114 | "Import task successfully complete!": "Импорт данных успешно завершён!", 115 | 116 | "Dump parent categories": "Выгружать родительские категории", 117 | "Export images mode": "Режим экспорта изображений", 118 | "Create images ZIP": "Создать ZIP архив с изображениями", 119 | "Render CSV header": "Добавить CSV заголовки", 120 | "CSV header": "CSV заголовок", 121 | "Control": "Управление", 122 | "Add new db field binding": "Добавить новую привязку к полю БД", 123 | "There is no created bindings": "Нет добавленных привязок", 124 | "Reset db field bindings": "Сбросить привязки к полям", 125 | "Any status": "Любой статус", 126 | "Export with status": "Выгружать со статусом", 127 | "Attributes encoder": "Кодировщик атрибутов", 128 | "Attributes export": "Экспорт атрибутов", 129 | 130 | "Check your download links:": "Ваши ссылки для скачивания файлов:", 131 | "Additional processing settings": "Настройки дополнительной обработки", 132 | "Multicolumn fields settings": "Несколько колонок в одно поле", 133 | "Preprocess value template": "Шаблон преобразования значения", 134 | "CSV file not selected": "Файл CSV не выбран", 135 | "Template variables": "Переменные шаблона", 136 | "Template variable name": "Имя переменной в шаблоне", 137 | "Add new template variable": "Добавить новую переменную шаблона", 138 | "Remove template variable": "Удалить переменную шаблона", 139 | 140 | "Replace CSV field data": "Заменить значение из CSV поля", 141 | "Add after CSV field data": "Добавить после значения CSV поля", 142 | "Add before CSV field data": "Добавить до значения CSV поля", 143 | 144 | "Info": "Информация", 145 | "Application information": "Информация о ПО", 146 | "How to use": "Как пользоваться", 147 | "Disclaimer": "Дисклеймер", 148 | "Module {name} targets to be a maximum flexible for import/export CSV data, without any restrictions in CSV format.": "Модуль {name} задумывался как максимально гибкое решение для импорта/экспорта данных. Нет каких-либо ограничений на формат входных CSV файлов.", 149 | "However, more flexibility costs more complexity, so if you not sure what you do, please, entrust the work to more qualified employee!": "Однако, такая гибкость сопряжена с довольно высокой сложностью использования, поэтому, если вы не уверены в своей квалификации, лучше доверьте работу с модулем более квалифицированному сотруднику!", 150 | "Please, look for instructions below": "Для получения дополнительной информации - заходите на страницу проекта - freeocart.ru", 151 | "Thank you for choosing {name} opencart module!": "Спасибо за выбор модуля {name}!", 152 | "I hope, my work will be useful for you": "Я надеюсь, моя работа была полезна для вас", 153 | "If you have any questions, found bugs, or want request a features, you can send me email, or use github issues to inform me.": "Если у вас есть вопросы, вы нашли ошибки, или хотите новые возможности, пожалуйста, пришлите мне email, или отпишитесь в issues на github.", 154 | "focsv-support@freeocart.ru": "support@freeocart.ru", 155 | "Use this software at your own risk!": "Используйте данное ПО на свой страх и риск!", 156 | "Author or contributors are not responsible at data corruption/remove caused by using this software.": "Ни автор, ни контрибьюторы проекта не отвечают за порчу/удаление данных, возникшие при использовании этого ПО.", 157 | "We are strongly recommend you to backup data before using this.": "Мы настоятельно рекомендуем делать бэкап перед использованием модуля.", 158 | "Source code distributed 'as is', all parts of the software is free and opensource and respecting GPLv3 license.": "Код распространяется 'как есть', все части ПО являются открытым программным обеспечением, распространяемым согласно лицензии GPLv3." 159 | } 160 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import resource from 'vue-resource' 5 | import App from './App' 6 | import router from './router' 7 | import i18n from './i18n' 8 | 9 | import store from './store' 10 | import ApiProvider from './api' 11 | import { validateAppConfig } from './helpers' 12 | 13 | Vue.use(resource) 14 | Vue.config.productionTip = false 15 | 16 | let AppConfig = {} 17 | 18 | if (window.FOC_CSV_PARAMS) { 19 | AppConfig = Object.assign(AppConfig, window.FOC_CSV_PARAMS) 20 | } 21 | 22 | if (validateAppConfig(AppConfig.requestConfig)) { 23 | // configure api provider 24 | let api = ApiProvider.prepare(AppConfig.requestConfig) 25 | Vue.use(api) 26 | 27 | // set initial data in vuex modules 28 | store.dispatch('importer/setInitialData', Object.assign({}, AppConfig.initial.importer, AppConfig.initial.common)) 29 | store.dispatch('exporter/setInitialData', Object.assign({}, AppConfig.initial.exporter, AppConfig.initial.common)) 30 | 31 | const language = AppConfig.language || 'en' 32 | /* eslint-disable no-new */ 33 | Vue.prototype.$appName = AppConfig.appName 34 | Vue.prototype.$appVersion = AppConfig.appVersion 35 | 36 | new Vue({ 37 | el: '#foc_csv', 38 | router, 39 | store, 40 | i18n: i18n(language), 41 | components: { App }, 42 | template: '' 43 | }) 44 | } 45 | else { 46 | console.error('Failed to initialize! Wrong AppConfig!') 47 | } 48 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/mixins/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | Util import/export mixin 3 | used in util components both by export and import tools 4 | */ 5 | import { createNamespacedHelpers } from 'vuex' 6 | 7 | export default function (module) { 8 | const { mapState, mapGetters, mapActions } = createNamespacedHelpers(module) 9 | 10 | return { 11 | computed: { 12 | ...mapState([ 13 | 'data' 14 | ]), 15 | ...mapGetters([ 16 | 'profile', 17 | 'profiles' 18 | ]) 19 | }, 20 | methods: { 21 | restoreToProfile ({ name, profile }) { 22 | this.applyProfile({ name, profile }) 23 | this.saveNewProfile(name) 24 | }, 25 | deleteProfile (name) { 26 | if (confirm(this.$t('Are you sure you want remove this item?'))) { 27 | if (this.currentProfileName === name) { 28 | this.setCurrentProfileName('default') 29 | } 30 | 31 | this.storeDeleteProfile(name) 32 | this.saveAllProfiles(this.profiles) 33 | } 34 | }, 35 | restoreProfiles (profiles) { 36 | if (confirm(this.$t('Are you sure? This will remove all profiles before trying to add new ones!'))) { 37 | this.saveAllProfiles(profiles) 38 | } 39 | }, 40 | ...mapActions([ 41 | 'saveAllProfiles', 42 | 'setCurrentProfileName', 43 | 'applyProfile', 44 | 'saveNewProfile' 45 | ]), 46 | ...mapActions({ 47 | storeDeleteProfile: 'deleteProfile' 48 | }) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Import from '@/components/importer/Import' 4 | import Export from '@/components/exporter/Export' 5 | import BackupRestore from '@/components/util/BackupRestore' 6 | import Info from '@/components/info/Info' 7 | 8 | Vue.use(Router) 9 | 10 | export default new Router({ 11 | routes: [ 12 | { 13 | path: '/', 14 | redirect: '/import' 15 | }, 16 | { 17 | path: '/import', 18 | name: 'Import', 19 | component: Import 20 | }, 21 | { 22 | path: '/export', 23 | name: 'Export', 24 | component: Export 25 | }, 26 | { 27 | path: '/util', 28 | name: 'Backup/Restore', 29 | component: BackupRestore 30 | }, 31 | { 32 | path: '/info', 33 | name: 'Info', 34 | component: Info 35 | } 36 | ] 37 | }) 38 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/common/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | Actions for both importer and exporter 3 | */ 4 | import { DEFAULT_PROFILE_NAME } from '@/config' 5 | 6 | export default { 7 | setInitialData ({ commit, getters }, data) { 8 | commit('SET_INITIAL_DATA', data) 9 | commit('SET_CURRENT_PROFILE_NAME', DEFAULT_PROFILE_NAME) 10 | commit('SET_CURRENT_PROFILE', getters.currentProfile) 11 | }, 12 | setCurrentProfileName ({ commit, getters }, profile) { 13 | commit('SET_CURRENT_PROFILE_NAME', profile) 14 | commit('SET_CURRENT_PROFILE', getters.currentProfile) 15 | }, 16 | applyProfile ({ commit }, { name, profile }) { 17 | commit('ADD_PROFILE', { name, profile }) 18 | commit('SET_CURRENT_PROFILE_NAME', name) 19 | }, 20 | deleteProfile ({ commit }, name) { 21 | commit('DELETE_PROFILE', name) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/common/getters.js: -------------------------------------------------------------------------------- 1 | /* 2 | Getters for both importer and exporter 3 | */ 4 | import { DEFAULT_PROFILE_NAME } from '@/config' 5 | 6 | export default { 7 | currentProfileName (state) { 8 | return state.currentProfile 9 | }, 10 | currentProfile (state) { 11 | let profileData = state.data.profiles[state.currentProfile] 12 | 13 | if (!profileData) { 14 | profileData = state.data.profiles[DEFAULT_PROFILE_NAME] 15 | } 16 | 17 | return profileData 18 | }, 19 | profile (state) { 20 | return state.profile 21 | }, 22 | encodings (state) { 23 | return state.data.encodings 24 | }, 25 | dbFields (state) { 26 | return state.data.dbFields 27 | }, 28 | languages (state) { 29 | return state.data.languages 30 | }, 31 | stores (state) { 32 | return state.data.stores 33 | }, 34 | profiles (state) { 35 | return state.data.profiles 36 | }, 37 | statuses (state) { 38 | return state.data.statuses 39 | }, 40 | stock_statuses (state) { 41 | return state.data.stock_statuses 42 | }, 43 | statusRewrites (state) { 44 | return state.profile.statusRewrites || {} 45 | }, 46 | stockStatusRewrites (state) { 47 | return state.profile.stockStatusRewrites || {} 48 | }, 49 | submittableData (state) { 50 | return { 51 | profile: state.profile, 52 | data: state.data 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/common/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | SET_INITIAL_DATA (state, initial) { 5 | Vue.set(state, 'data', initial) 6 | }, 7 | SET_CURRENT_PROFILE_NAME (state, profileName) { 8 | state.currentProfile = profileName 9 | }, 10 | SET_CURRENT_PROFILE (state, profile) { 11 | state.profile = profile 12 | }, 13 | SAVE_NEW_PROFILE (state, name) { 14 | let profileSettings = Object.assign({}, state.profile) 15 | Vue.set(state.data.profiles, name, profileSettings) 16 | }, 17 | ADD_PROFILE (state, { name, profile }) { 18 | Vue.set(state.data.profiles, name, profile) 19 | }, 20 | DELETE_PROFILE (state, name) { 21 | if (state.data.profiles[name]) { 22 | Vue.delete(state.data.profiles, name) 23 | } 24 | }, 25 | SET_PROFILES (state, profiles) { 26 | Vue.set(state.data, 'profiles', profiles) 27 | }, 28 | CLEAR_ALL_PROFILES (state) { 29 | Vue.set(state.data, 'profiles', {}) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/exporter/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | Actions for exporter module 3 | */ 4 | 5 | import Vue from 'vue' 6 | import commonActions from '@/store/common/actions' 7 | 8 | export default { 9 | ...commonActions, 10 | setAttributeEncoder ({ commit }, encoder) { 11 | commit('SET_ATTRIBUTE_ENCODER', encoder) 12 | }, 13 | setAttributeEncoderData ({ commit }, data) { 14 | commit('SET_ATTRIBUTE_ENCODER_DATA', data) 15 | }, 16 | async saveNewProfile ({ commit, state }, name) { 17 | try { 18 | await Vue.$api.exporter.saveProfile({ 19 | name, 20 | profile: state.profile 21 | }) 22 | 23 | commit('SAVE_NEW_PROFILE', name) 24 | commit('SET_CURRENT_PROFILE_NAME', name) 25 | } 26 | catch (e) { 27 | console.log(e) 28 | alert('error on profile saving!') 29 | } 30 | }, 31 | async saveAllProfiles ({ commit }, profiles) { 32 | await Vue.$api.exporter.saveProfiles(profiles) 33 | 34 | commit('CLEAR_ALL_PROFILES') 35 | commit('SET_PROFILES', profiles) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/exporter/getters.js: -------------------------------------------------------------------------------- 1 | import commonGetters from '@/store/common/getters' 2 | 3 | export default { 4 | attributeEncoders (state) { 5 | return state.data.attributeEncoders 6 | }, 7 | currentAttributeEncoder (state) { 8 | return state.profile.attributeEncoder 9 | }, 10 | attributeEncoderOptions (state) { 11 | let encoder = state.profile.attributeEncoder 12 | 13 | if (!encoder || !state.data.attributeEncoders[encoder]) { 14 | return [] 15 | } 16 | 17 | if (state.data.attributeEncoders[encoder].options) { 18 | return state.data.attributeEncoders[encoder].options 19 | } 20 | 21 | return [] 22 | }, 23 | attributeEncoderOptionData (state) { 24 | let encoder = state.profile.attributeEncoder 25 | return state.profile.attributeEncoderData[encoder] || [] 26 | }, 27 | ...commonGetters 28 | } 29 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/exporter/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import mutations from './mutations' 3 | import actions from './actions' 4 | import getters from './getters' 5 | 6 | import { genVuexModels } from 'vuex-models' 7 | 8 | let fields = genVuexModels([ 9 | 'entriesPerQuery', 10 | 'encoding', 11 | 'dumpParentCategories', 12 | 'categoriesNestingDelimiter', 13 | 'categoriesDelimiter', 14 | 'exportImagesMode', 15 | 'createImagesZIP', 16 | 'galleryImagesDelimiter', 17 | 'csvFieldDelimiter', 18 | 'store', 19 | 'language', 20 | 'csvFieldDelimiter', 21 | 'csvHeader', 22 | 'bindings', 23 | 'exportWithStatus' 24 | ], 'profile') 25 | 26 | let exportProgress = genVuexModels([ 27 | 'exportJobTotal', 28 | 'exportJobCurrent', 29 | 'exportJobWorking' 30 | ], 'exportJob') 31 | 32 | export default { 33 | namespaced: true, 34 | state, 35 | mutations: { 36 | ...mutations, 37 | ...fields.mutations, 38 | ...exportProgress.mutations 39 | }, 40 | actions: { 41 | ...actions, 42 | ...fields.actions, 43 | ...exportProgress.actions 44 | }, 45 | getters: { 46 | ...getters, 47 | ...fields.getters, 48 | ...exportProgress.getters 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/exporter/mutation-types.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freeocart/fo-csv/6dcd8eaa1b125c873f9637ba93fbfd634622e02e/frontend/foc_csv/src/store/exporter/mutation-types.js -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/exporter/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import commonMutations from '@/store/common/mutations' 3 | 4 | export default { 5 | SET_ATTRIBUTE_ENCODER (state, encoder) { 6 | Vue.set(state.profile, 'attributeEncoder', encoder) 7 | 8 | if (!encoder) { 9 | return 10 | } 11 | 12 | if (!state.profile.attributeEncoderData || !state.profile.attributeEncoderData[encoder]) { 13 | Vue.set(state.profile, 'attributeEncoderData', { 14 | [encoder]: {} 15 | }) 16 | } 17 | 18 | let encoderObj = state.data.attributeEncoders[encoder] 19 | 20 | if (encoderObj.options) { 21 | for (let key in encoderObj.options) { 22 | if (!state.profile.attributeEncoderData[encoder][key] && encoderObj.options[key].default) { 23 | Vue.set(state.profile.attributeEncoderData[encoder], key, encoderObj.options[key].default) 24 | } 25 | } 26 | } 27 | }, 28 | SET_ATTRIBUTE_ENCODER_DATA (state, [ key, value ]) { 29 | let encoder = state.profile.attributeEncoder 30 | Vue.set(state.profile.attributeEncoderData[encoder], key, value) 31 | }, 32 | ...commonMutations 33 | } 34 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/exporter/state.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_PROFILE_NAME } from '@/config' 2 | 3 | export default { 4 | data: {}, 5 | profile: { 6 | exportWithStatus: -1 7 | }, 8 | exportJob: { 9 | exportJobWorking: false, 10 | exportJobCurrent: 0, 11 | exportJobTotal: 0 12 | }, 13 | currentProfile: DEFAULT_PROFILE_NAME 14 | } 15 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/importer/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | Actions for importer module 3 | */ 4 | 5 | import Vue from 'vue' 6 | import commonActions from '@/store/common/actions' 7 | 8 | export default { 9 | ...commonActions, 10 | setCsvFieldNames ({ commit }, fieldNames) { 11 | commit('SET_CSV_FIELD_NAMES', fieldNames) 12 | }, 13 | setBindings ({ commit }, bindings) { 14 | commit('SET_DB_TO_CSV_BINDINGS', bindings) 15 | }, 16 | setCsvFileRef ({ commit }, ref) { 17 | commit('SET_CSV_FILE_REF', ref) 18 | }, 19 | setImagesZipRef ({ commit }, ref) { 20 | commit('SET_IMAGES_ZIP_FILE_REF', ref) 21 | }, 22 | async saveNewProfile ({ commit, state }, name) { 23 | try { 24 | await Vue.$api.importer.saveProfile({ 25 | name, 26 | profile: state.profile 27 | }) 28 | 29 | commit('SAVE_NEW_PROFILE', name) 30 | commit('SET_CURRENT_PROFILE_NAME', name) 31 | } 32 | catch (e) { 33 | console.log(e) 34 | alert('error on profile saving!') 35 | } 36 | }, 37 | setStockStatusRewriteRule ({ commit }, rule) { 38 | commit('SET_STOCK_STATUS_REWRITE_RULE', rule) 39 | }, 40 | setStatusRewriteRule ({ commit }, rule) { 41 | commit('SET_STATUS_REWRITE_RULE', rule) 42 | }, 43 | setAttributeParser ({ commit }, parser) { 44 | commit('SET_ATTRIBUTE_PARSER', parser) 45 | }, 46 | setAttributeParserData ({ commit }, data) { 47 | commit('SET_ATTRIBUTE_PARSER_DATA', data) 48 | }, 49 | async saveAllProfiles ({ commit }, profiles) { 50 | await Vue.$api.importer.saveProfiles(profiles) 51 | 52 | commit('CLEAR_ALL_PROFILES') 53 | commit('SET_PROFILES', profiles) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/importer/getters.js: -------------------------------------------------------------------------------- 1 | /* 2 | Getters for exporter module 3 | */ 4 | 5 | import commonGetters from '@/store/common/getters' 6 | 7 | export default { 8 | ...commonGetters, 9 | csvFields (state) { 10 | return state.data.csvFields 11 | }, 12 | csvFileRef (state) { 13 | return state.data.csvFileRef 14 | }, 15 | keyFields (state) { 16 | return state.data.keyFields 17 | }, 18 | attributeParsers (state) { 19 | return state.data.attributeParsers 20 | }, 21 | currentAttributeParser (state) { 22 | return state.profile.attributeParser 23 | }, 24 | attributeParserOptions (state) { 25 | let parser = state.profile.attributeParser 26 | 27 | if (!parser || !state.data.attributeParsers[parser]) { 28 | return [] 29 | } 30 | 31 | if (state.data.attributeParsers[parser].options) { 32 | return state.data.attributeParsers[parser].options 33 | } 34 | 35 | return [] 36 | }, 37 | attributeParserOptionData (state) { 38 | let parser = state.profile.attributeParser 39 | return state.profile.attributeParserData[parser] || [] 40 | }, 41 | categoryDelimiter (state) { 42 | return state.profile.categoryDelimiter 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/importer/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import mutations from './mutations' 3 | import actions from './actions' 4 | import getters from './getters' 5 | 6 | import { genVuexModels } from 'vuex-models' 7 | 8 | /* 9 | Generate mutation/action/getters for profile fields 10 | */ 11 | let fields = genVuexModels([ 12 | 'keyField', 13 | 'fillParentCategories', 14 | 'skipLines', 15 | 'csvWithoutHeaders', 16 | 'csvHeadersLineNumber', 17 | 'store', 18 | 'language', 19 | 'importMode', 20 | 'encoding', 21 | 'defaultStatus', 22 | 'defaultStockStatus', 23 | 'categoryDelimiter', 24 | 'imagesImportMode', 25 | 'categoryLevelDelimiter', 26 | 'processAtStepNum', 27 | 'downloadImages', 28 | 'clearGalleryBeforeImport', 29 | 'previewFromGallery', 30 | 'csvImageFieldDelimiter', 31 | 'csvFieldDelimiter', 32 | 'removeCharsFromCategory', 33 | 'removeManufacturersBeforeImport', 34 | 'defaultAttributesGroup', 35 | 'attributesCSVField', 36 | 'skipLineOnEmptyFields', 37 | 'multicolumnFields', 38 | 'replaceAttributes' 39 | ], 'profile') 40 | 41 | let importProgress = genVuexModels([ 42 | 'importJobTotal', 43 | 'importJobCurrent', 44 | 'importJobWorking' 45 | ], 'importJob') 46 | 47 | export default { 48 | namespaced: true, 49 | state, 50 | mutations: { 51 | ...fields.mutations, 52 | ...importProgress.mutations, 53 | ...mutations 54 | }, 55 | actions: { 56 | ...fields.actions, 57 | ...importProgress.actions, 58 | ...actions 59 | }, 60 | getters: { 61 | ...fields.getters, 62 | ...importProgress.getters, 63 | ...getters 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/importer/mutation-types.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freeocart/fo-csv/6dcd8eaa1b125c873f9637ba93fbfd634622e02e/frontend/foc_csv/src/store/importer/mutation-types.js -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/importer/mutations.js: -------------------------------------------------------------------------------- 1 | /* 2 | Mutations for exporter module 3 | */ 4 | 5 | import Vue from 'vue' 6 | import commonMutations from '@/store/common/mutations' 7 | 8 | export default { 9 | ...commonMutations, 10 | SET_STOCK_STATUS_REWRITE_RULE (state, { value, id }) { 11 | if (!state.profile.stockStatusRewrites) { 12 | Vue.set(state.profile, 'stockStatusRewrites', {}) 13 | } 14 | Vue.set(state.profile.stockStatusRewrites, id, value) 15 | }, 16 | SET_STATUS_REWRITE_RULE (state, { value, id }) { 17 | if (!state.profile.statusRewrites) { 18 | Vue.set(state.profile, 'statusRewrites', {}) 19 | } 20 | Vue.set(state.profile.statusRewrites, id, value) 21 | }, 22 | SET_CSV_FIELD_NAMES (state, fields) { 23 | Vue.set(state.data, 'csvFields', fields) 24 | }, 25 | SET_CATEGORY_DELIMITER (state, delimiter) { 26 | Vue.set(state.profile, 'categoryDelimiter', delimiter) 27 | }, 28 | SET_DB_TO_CSV_BINDINGS (state, bindings) { 29 | Vue.set(state.profile, 'bindings', bindings) 30 | }, 31 | SET_CSV_FILE_REF (state, ref) { 32 | Vue.set(state.data, 'csvFileRef', ref) 33 | }, 34 | SET_IMAGES_ZIP_FILE_REF (state, ref) { 35 | Vue.set(state.data, 'imagesZipFileRef', ref) 36 | }, 37 | SET_ATTRIBUTE_PARSER (state, parser) { 38 | Vue.set(state.profile, 'attributeParser', parser) 39 | 40 | if (!parser) { 41 | return 42 | } 43 | 44 | if (!state.profile.attributeParserData || !state.profile.attributeParserData[parser]) { 45 | Vue.set(state.profile, 'attributeParserData', { 46 | [parser]: {} 47 | }) 48 | } 49 | 50 | let parserObj = state.data.attributeParsers[parser] 51 | 52 | if (parserObj.options) { 53 | for (let key in parserObj.options) { 54 | if (!state.profile.attributeParserData[parser][key] && parserObj.options[key].default) { 55 | Vue.set(state.profile.attributeParserData[parser], key, parserObj.options[key].default) 56 | } 57 | } 58 | } 59 | }, 60 | SET_ATTRIBUTE_PARSER_DATA (state, [ key, value ]) { 61 | let parser = state.profile.attributeParser 62 | Vue.set(state.profile.attributeParserData[parser], key, value) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/importer/state.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_PROFILE_NAME } from '@/config' 2 | 3 | export default { 4 | importJob: { 5 | importJobWorking: false, 6 | importJobCurrent: 0, 7 | importJobTotal: 0 8 | }, 9 | urls: { 10 | import: '' 11 | }, 12 | currentProfile: DEFAULT_PROFILE_NAME, 13 | data: {}, 14 | profile: { 15 | statusRewrites: {}, 16 | stockStatusRewrites: {}, 17 | multicolumnFields: [] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import importer from './importer' 4 | import exporter from './exporter' 5 | 6 | Vue.use(Vuex) 7 | 8 | const store = new Vuex.Store({ 9 | modules: { 10 | importer, 11 | exporter 12 | } 13 | }) 14 | 15 | export default store 16 | -------------------------------------------------------------------------------- /frontend/foc_csv/src/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestConfig": { 3 | "token": "jjj", 4 | "baseRoute": "ii", 5 | "baseUrl": "kk" 6 | }, 7 | "initial": { 8 | "data": { 9 | "keyFields": [ 10 | "product_id", 11 | "sku", 12 | "model" 13 | ], 14 | "profiles": { 15 | "default": { 16 | "encoding": "UTF8", 17 | "csvFieldDelimiter": ";", 18 | "categoryDelimiter": "/", 19 | "keyField": "product_id", 20 | "skipFirstLine": 1, 21 | "csvToDbBindings": { 22 | "Idx": "product.product_id" 23 | }, 24 | "bindings": { 25 | "5": "nothing", 26 | "6": "name", 27 | "10": "sku" 28 | }, 29 | "importMode": "updateCreate", 30 | "csvImageFieldDelimiter": ";" 31 | } 32 | }, 33 | "encodings": ["UTF8", "CP1251"], 34 | "csvFields": [], 35 | "dbFields": { 36 | "other": ["nothing"], 37 | "product": ["product_id", "name"], 38 | "product_description": ["product_description"] 39 | }, 40 | "fileRef": {} 41 | }, 42 | "csvFields": ["_CATEGORY_", "_NAME_", "_MODEL_", "_SKU_", "_MANUFACTURER_", "_PRICE_", "_QUANTITY_", "_STOCK_STATUS_", "_META_TITLE_", "_META_KEYWORDS_", "_META_DESCRIPTION_", "_DESCRIPTION_", "_PRODUCT_TAG_", "_IMAGE_", "_SORT_ORDER_", "_STATUS_", "_SEO_KEYWORD_", "_ATTRIBUTES_", "_IMAGES_", "_URL_"], 43 | "csvFileRef": {} 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/foc_csv/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freeocart/fo-csv/6dcd8eaa1b125c873f9637ba93fbfd634622e02e/frontend/foc_csv/static/.gitkeep -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const { task, series, src, dest, watch } = require('gulp') 2 | const zip = require('gulp-zip') 3 | const { name } = require('./package.json') 4 | 5 | const BUILD_ZIP_NAME = `${name}.ocmod.zip` 6 | const SITE_PATH = process.env.SITE_DIR || './compiled-files' 7 | 8 | // globs: 9 | const GLOB_INSTALL_XML = './install.xml' 10 | const GLOB_SOURCE_BASE = './upload/**' 11 | // we dont need it at now as distribution is universal 12 | // const SOURCE_TEMPLATES_EXCLUDE_GLOB = process.env.TARGET_PLATFORM === '2' ? `!${GLOB_SOURCE_BASE}/*.twig` : `!${GLOB_SOURCE_BASE}/*.tpl` 13 | const GLOB_EXCLUDES = [ 14 | '!**/.DS_Store', 15 | '!**/.git', 16 | ] 17 | 18 | const copyFiles = () => 19 | src([ GLOB_SOURCE_BASE, ...GLOB_EXCLUDES ]) 20 | .pipe(dest(SITE_PATH)) 21 | 22 | const initWatcher = () => 23 | watch([ GLOB_SOURCE_BASE, ...GLOB_EXCLUDES ], copyFiles) 24 | 25 | const packFiles = () => 26 | src([ GLOB_SOURCE_BASE, GLOB_INSTALL_XML, ...GLOB_EXCLUDES ], { base: '.' }) 27 | .pipe(zip(BUILD_ZIP_NAME)) 28 | .pipe(dest('./')) 29 | 30 | /* 31 | Tasks: 32 | */ 33 | task('pack:files', packFiles) 34 | 35 | task('copy:files', copyFiles) 36 | 37 | task('watch:files', series(copyFiles, initWatcher)) 38 | 39 | task('build', packFiles) -------------------------------------------------------------------------------- /install.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | fo_csv_menu_link 4 | fo_csv_menu_link_id 5 | 2.1.0.0 6 | ikenfin 7 | http://freeocart.ru 8 | 9 | 10 | user->hasPermission('access', 'tool/backup')) {]]> 11 | user->hasPermission('access', 'extension/module/foc_csv')) { 13 | $item = array( 14 | 'name' => $this->language->get('text_foc_csv'), 15 | 'children' => array() 16 | ); 17 | 18 | // oc 2 19 | if (isset($tool) && is_array($tool)) { 20 | $item['href'] = $this->url->link('extension/module/foc_csv', 'token=' . $this->session->data['token'], true); 21 | $tool[] = $item; 22 | } 23 | // oc 3 24 | elseif (isset($maintenance) && is_array($maintenance)) { 25 | $item['href'] = $this->url->link('extension/module/foc_csv', 'user_token=' . $this->session->data['user_token'], true); 26 | $maintenance[] = $item; 27 | } 28 | } 29 | ]]> 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foc_csv", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "gulp watch:files", 8 | "dev:build": "npm run build:frontend && gulp copy:files", 9 | "pack": "gulp pack:files", 10 | "prebuild": "yarn run --cwd=./frontend/foc_csv build --prod", 11 | "build": "gulp build", 12 | "build:frontend": "yarn run --cwd=./frontend/foc_csv build" 13 | }, 14 | "dependencies": { 15 | "gulp": "^4.0.2", 16 | "gulp-zip": "^5.0.2" 17 | }, 18 | "devDependencies": { 19 | "yarn": "^1.22.10" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /upload/admin/controller/extension/module/foc_csv.php: -------------------------------------------------------------------------------- 1 | load->model('extension/module/foc_csv_common'); 11 | $this->load->model('extension/module/foc_csv'); 12 | $this->load->model('extension/module/foc_csv_exporter'); 13 | 14 | // Remove unnecessary template version 15 | $templatePath = DIR_APPLICATION . 'view/template/extension/module/'; 16 | $viewFile = 'foc_csv.twig'; 17 | 18 | if ($this->model_extension_module_foc_csv_common->isOpencart3()) { 19 | $viewFile = 'foc_csv.php'; 20 | } 21 | 22 | if (is_file($templatePath . $viewFile)) { 23 | unlink($templatePath . $viewFile); 24 | } 25 | 26 | $this->model_extension_module_foc_csv->install(); 27 | $this->model_extension_module_foc_csv_exporter->install(); 28 | } 29 | 30 | private function sendOk ($message = '') { 31 | echo json_encode(array( 32 | 'status' => 'ok', 33 | 'message' => $message 34 | ), JSON_HEX_AMP); 35 | die; 36 | } 37 | 38 | private function sendFail ($message = '') { 39 | echo json_encode(array( 40 | 'status' => 'fail', 41 | 'message' => $message 42 | )); 43 | die; 44 | } 45 | 46 | private function getTokenName () { 47 | $this->load->model('extension/module/foc_csv_common'); 48 | if ($this->model_extension_module_foc_csv_common->isOpencart3()) { 49 | return 'user_token'; 50 | } 51 | return 'token'; 52 | } 53 | 54 | private function getToken () { 55 | return $this->session->data[$this->getTokenName()]; 56 | } 57 | 58 | private function createUrl ($path, $params = '') { 59 | return $this->url->link($path, $this->getTokenName() . '=' . $this->getToken() . '&' . $params, 'ssl'); 60 | } 61 | 62 | public function index () { 63 | $data = array(); 64 | 65 | $this->language->load('extension/module/foc_csv'); 66 | $this->document->setTitle($this->language->get('heading_title')); 67 | $data['heading_title'] = $this->language->get('heading_title'); 68 | 69 | $data['foc_app_preload_title'] = $this->language->get('foc_app_preload_title'); 70 | $data['foc_app_preload_description'] = $this->language->get('foc_app_preload_description'); 71 | 72 | $data['tokenName'] = $this->getTokenName(); 73 | $data['token'] = $this->getToken(); 74 | $data['baseRoute'] = 'extension/module/foc_csv'; 75 | $data['baseUrl'] = $this->url->link('', '', 'ssl'); 76 | 77 | $this->load->model('extension/module/foc_csv_common'); 78 | $this->load->model('extension/module/foc_csv'); 79 | $this->load->model('extension/module/foc_csv_exporter'); 80 | $this->load->model('setting/store'); 81 | $this->load->model('localisation/language'); 82 | $this->load->model('localisation/stock_status'); 83 | 84 | $data['language'] = $this->model_extension_module_foc_csv_common->getLanguageCode(); 85 | 86 | $data['breadcrumbs'] = $this->breadcrumbs(); 87 | 88 | $this->document->addStyle('view/javascript/foc_csv/css/app.css'); 89 | $this->document->addScript('view/javascript/foc_csv/js/manifest.js', 'foc_csv_js'); 90 | $this->document->addScript('view/javascript/foc_csv/js/vendor.js', 'foc_csv_js'); 91 | $this->document->addScript('view/javascript/foc_csv/js/app.js', 'foc_csv_js'); 92 | 93 | $data['header'] = $this->load->controller('common/header'); 94 | $data['footer'] = $this->load->controller('common/footer'); 95 | $data['column_left'] = $this->load->controller('common/column_left'); 96 | 97 | $data['scripts'] = $this->document->getScripts('foc_csv_js'); 98 | 99 | $common = array(); 100 | $importer = array(); 101 | $exporter = array(); 102 | 103 | $data['foc_version'] = $this->model_extension_module_foc_csv->getVersion(); 104 | 105 | $importer['profiles'] = $this->model_extension_module_foc_csv->loadProfiles(); 106 | $importer['keyFields'] = $this->model_extension_module_foc_csv->getKeyFields(); 107 | $common['encodings'] = array('none', 'UTF8', 'cp1251'); 108 | $common['dbFields'] = $this->model_extension_module_foc_csv->getDbFields(); 109 | 110 | $exporter['profiles'] = $this->model_extension_module_foc_csv_exporter->loadProfiles(); 111 | 112 | /* stores */ 113 | $common['stores'] = array(); 114 | $common['stores'][] = array( 115 | 'name' => $this->config->get('config_name'), 116 | 'id' => $this->config->get('config_store_id') 117 | ); 118 | 119 | $stores = $this->model_setting_store->getStores(); 120 | foreach ($stores as $store) { 121 | $common['stores'][] = array( 122 | 'name' => $store['name'], 123 | 'id' => $store['store_id'] 124 | ); 125 | } 126 | 127 | /* available statuses */ 128 | $common['stock_statuses'] = array(); 129 | $statuses = $this->model_localisation_stock_status->getStockStatuses(); 130 | 131 | foreach ($statuses as $status) { 132 | $common['stock_statuses'][] = array( 133 | 'id' => $status['stock_status_id'], 134 | 'name' => $status['name'] 135 | ); 136 | } 137 | 138 | $common['statuses'] = array( 139 | array( 140 | 'id' => 0, 141 | 'name' => $this->language->get('text_disabled') 142 | ), 143 | array( 144 | 'id' => 1, 145 | 'name' => $this->language->get('text_enabled') 146 | ) 147 | ); 148 | 149 | $common['languages'] = array(); 150 | $languages = $this->model_localisation_language->getLanguages(); 151 | foreach ($languages as $lang) { 152 | $common['languages'][] = array( 153 | 'name' => $lang['name'], 154 | 'id' => $lang['language_id'] 155 | ); 156 | } 157 | 158 | $importer['attributeParsers'] = $this->model_extension_module_foc_csv->getAttributeParsers(); 159 | 160 | $exporter['attributeEncoders'] = $this->model_extension_module_foc_csv_exporter->getAttributeEncoders(); 161 | 162 | $data['initial'] = json_encode(array( 163 | 'importer' => $importer, 164 | 'exporter' => $exporter, 165 | 'common' => $common 166 | )); 167 | 168 | return $this->response->setOutput($this->load->view('extension/module/foc_csv', $data)); 169 | } 170 | 171 | /* 172 | Load profile settings from DB 173 | */ 174 | public function loadProfile () { 175 | $type = null; 176 | 177 | if (isset($this->request->get['type'])) { 178 | $type = $this->request->get['type']; 179 | } 180 | else { 181 | $this->sendFail('Profile type not specified!'); 182 | } 183 | 184 | $this->load->model('extension/module/foc_csv_common'); 185 | 186 | $name = 'default'; 187 | 188 | if (isset($this->request->get['profile'])) { 189 | $name = $this->request->get['profile']; 190 | } 191 | 192 | $profile = null; 193 | 194 | if ($type === 'importer') { 195 | $this->load->model('extension/module/foc_csv'); 196 | $profile = $this->extension_module_foc_csv->loadProfile($name); 197 | } 198 | else { 199 | $this->load->model('extension/module/foc_csv_exporter'); 200 | $profile = $this->extension_module_foc_csv_exporter->loadProfile($name); 201 | } 202 | 203 | if (is_null($profile)) { 204 | $this->sendFail('Profile [' . $name . '] not found!'); 205 | } 206 | 207 | echo json_encode($profile); 208 | die; 209 | } 210 | 211 | /* 212 | Save profile to DB 213 | */ 214 | public function saveProfile () { 215 | $type = null; 216 | if (isset($this->request->get['type'])) { 217 | $type = $this->request->get['type']; 218 | } 219 | else { 220 | $this->sendFail('Profile type not specified!'); 221 | } 222 | 223 | if ($this->request->server['REQUEST_METHOD'] == 'POST') { 224 | $json = json_decode(file_get_contents('php://input'), true); 225 | 226 | if (isset($json['name']) && isset($json['profile'])) { 227 | $this->load->model('extension/module/foc_csv_common'); 228 | 229 | $name = $json['name']; 230 | $profile = $json['profile']; 231 | 232 | if ($type == 'importer') { 233 | $this->load->model('extension/module/foc_csv'); 234 | $this->model_extension_module_foc_csv->setProfile($name, $profile); 235 | } 236 | else { 237 | $this->load->model('extension/module/foc_csv_exporter'); 238 | $this->model_extension_module_foc_csv_exporter->setProfile($name, $profile); 239 | } 240 | 241 | $this->sendOk(); 242 | } 243 | 244 | $this->sendFail('No required data present!'); 245 | } 246 | } 247 | 248 | public function saveProfiles () { 249 | $type = null; 250 | if (isset($this->request->get['type'])) { 251 | $type = $this->request->get['type']; 252 | } 253 | else { 254 | $this->sendFail('Profile type not specified!'); 255 | } 256 | 257 | if ($this->request->server['REQUEST_METHOD'] == 'POST') { 258 | $json = json_decode(file_get_contents('php://input'), true); 259 | 260 | if (isset($json['profiles'])) { 261 | $this->load->model('extension/module/foc_csv_common'); 262 | } 263 | else { 264 | $this->sendFail('Profiles to save is not presented!'); 265 | } 266 | 267 | $profiles = array(); 268 | 269 | if ($type == 'importer') { 270 | $this->load->model('extension/module/foc_csv'); 271 | $this->model_extension_module_foc_csv->saveProfiles($json['profiles']); 272 | $profiles = $this->model_extension_module_foc_csv->loadProfiles(); 273 | } 274 | else { 275 | $this->load->model('extension/module/foc_csv_exporter'); 276 | $this->model_extension_module_foc_csv_exporter->saveProfiles($json['profiles']); 277 | $profiles = $this->model_extension_module_foc_csv_exporter->loadProfiles(); 278 | } 279 | 280 | $this->sendOk(json_encode($profiles)); 281 | } 282 | 283 | $this->sendFail(); 284 | } 285 | 286 | public function export () { 287 | if (isset($_POST['profile-json'])) { 288 | $this->load->model('extension/module/foc_csv_common'); 289 | $this->load->model('extension/module/foc_csv_exporter'); 290 | 291 | $profile = $this->model_extension_module_foc_csv_exporter->fillProfileEmptyValues(json_decode($_POST['profile-json'], true)); 292 | 293 | $this->model_extension_module_foc_csv_exporter->applyProfile($profile); 294 | 295 | $key = $this->model_extension_module_foc_csv_exporter->prepareUploadPath(); 296 | $exportFile = $this->model_extension_module_foc_csv_exporter->getExportCsvFilePath($key); 297 | $exportImagesFile = null; 298 | 299 | $encoderEnabled = isset($profile['attributeEncoder']) && !is_null($profile['attributeEncoder']); 300 | 301 | if ($profile['createImagesZIP']) { 302 | $exportImagesFile = $this->model_extension_module_foc_csv_exporter->getExportImagesZipFilePath($key); 303 | } 304 | 305 | // put csv headers if need 306 | if ($profile['csvHeader']) { 307 | $csv_fid = fopen($exportFile, 'w'); 308 | $headers = array(); 309 | 310 | foreach ($profile['bindings'] as $binding) { 311 | $headers[] = $binding['header']; 312 | } 313 | 314 | // reserving columns for attributes 315 | // there is some tricky solution, based on keys order, but it seems to be ok 316 | // also we do it only if encoder is multicolumn! 317 | // Otherwise - just push attributes after last column 318 | if ($encoderEnabled) { 319 | // csvIdx => group_id:attribute_id 320 | $attributeHeaders = $this->model_extension_module_foc_csv_exporter->encodeAttributeHeaders($profile, count($headers)); 321 | 322 | // if encoder is multicolumn we get array 323 | // also using += to prevent keys reindexing 324 | if (is_array($attributeHeaders)) { 325 | $headers += $attributeHeaders; 326 | } 327 | else { 328 | $headers[] = $attributeHeaders; 329 | } 330 | } 331 | 332 | fputcsv($csv_fid, $headers, $profile['csvFieldDelimiter']); 333 | fclose($csv_fid); 334 | } 335 | 336 | $total = $this->model_extension_module_foc_csv_exporter->getProductTotal($profile); 337 | 338 | $export_url = $this->createUrl('extension/module/foc_csv/exportPart'); 339 | 340 | $response = array( 341 | 'total' => $total, 342 | 'key' => $key, 343 | 'exportUrl' => html_entity_decode($export_url), 344 | 'csvFileUrl' => html_entity_decode($this->createUrl('extension/module/foc_csv/download', 'key=' . $key . '&mode=' . self::KEY_FOR_CSV_FILE)), 345 | 'position' => 0 346 | ); 347 | 348 | if (!is_null($exportImagesFile)) { 349 | $response['imagesZipUrl'] = html_entity_decode($this->createUrl('extension/module/foc_csv/download', 'key=' . $key . '&mode=' . self::KEY_FOR_IMAGES_ZIP_FILE)); 350 | } 351 | 352 | $this->sendOk($response); 353 | } 354 | } 355 | 356 | public function exportPart () { 357 | if ($this->request->server['REQUEST_METHOD'] == 'POST') { 358 | $json = json_decode(file_get_contents('php://input'), true); 359 | 360 | $this->load->model('extension/module/foc_csv_common'); 361 | $this->load->model('extension/module/foc_csv_exporter'); 362 | 363 | if (isset($json['position']) 364 | && isset($json['key']) 365 | && isset($json['profile']) 366 | ) { 367 | $offset = $json['position']; 368 | $profile = $json['profile']; 369 | $key = $json['key']; 370 | $errors = isset($json['errors']) ? intval($json['errors']) : 0; 371 | $limit = $profile['entriesPerQuery']; 372 | 373 | // TODO: create initilize method and pass profile/key/other state data to it 374 | // also, refactor all file api to use state uploadKey, instead of passing as argument 375 | $this->model_extension_module_foc_csv_exporter->applyProfile($profile); 376 | $this->model_extension_module_foc_csv_exporter->setUploadKey($key); 377 | 378 | $exportFile = $this->model_extension_module_foc_csv_exporter->getExportCsvFilePath($key); 379 | 380 | $exportable = $this->model_extension_module_foc_csv_exporter->export($profile, $offset, $limit); 381 | 382 | $csv_fid = fopen($exportFile, 'a'); 383 | 384 | $db_charset = strtolower($this->model_extension_module_foc_csv_exporter->getDBCharset()); 385 | $charset = strtolower($this->model_extension_module_foc_csv_exporter->dbToIconvCharset($db_charset)); 386 | $encoding = strtolower($profile['encoding']); 387 | 388 | foreach ($exportable as $csvLine) { 389 | // try change file encoding 390 | if ($encoding !== 'none' && $charset !== $encoding) { 391 | $this->model_extension_module_foc_csv_exporter->writeLog('Trying to convert character encoding from [' . $charset . '] to [' . $encoding . ']'); 392 | 393 | if (!function_exists('iconv')) { 394 | $this->model_extension_module_foc_csv_exporter->writeLog('Please install iconv or convert csv file to [' . $encoding . ']', 'error'); 395 | $this->sendFail('Please install iconv or convert csv file to [' . $encoding . ']'); 396 | } 397 | 398 | foreach ($csvLine as $part_idx => $part) { 399 | $encoded = iconv($charset, $encoding . '//TRANSLIT//IGNORE', $part); 400 | 401 | if ($encoded) { 402 | $csvLine[$part_idx] = $encoded; 403 | } 404 | else { 405 | $this->model_extension_module_foc_csv_exporter->writeLog('Failed iconv from [' . $charset . '] to [' . $encoding . ']', 'error'); 406 | } 407 | } 408 | } 409 | 410 | fputcsv($csv_fid, $csvLine, $profile['csvFieldDelimiter']); 411 | } 412 | fclose($csv_fid); 413 | 414 | if ($profile['createImagesZIP'] 415 | && $this->model_extension_module_foc_csv_exporter->hasCollectedImages() 416 | ) { 417 | $exportImagesFile = $this->model_extension_module_foc_csv_exporter->getExportImagesZipFilePath($key); 418 | $zip = new ZipArchive(); 419 | $zip->open($exportImagesFile, ZipArchive::CREATE); 420 | 421 | foreach($this->model_extension_module_foc_csv_exporter->getCollectedImages() as $image) { 422 | $path = DIR_IMAGE . $image; 423 | $zip->addFile($path, $image); 424 | } 425 | 426 | $zip->close(); 427 | } 428 | 429 | $position = $offset + $limit; 430 | 431 | $response = array( 432 | 'key' => $key, 433 | 'position' => $position, 434 | 'errors' => $errors, 435 | 'collected_images' => $this->model_extension_module_foc_csv_exporter->getCollectedImagesCount() 436 | ); 437 | 438 | $this->sendOk($response); 439 | } 440 | } 441 | } 442 | 443 | /* 444 | Upload files and starting import 445 | */ 446 | public function import () { 447 | if (!empty($_FILES) && isset($_POST['profile-json'])) { 448 | $this->load->model('extension/module/foc_csv_common'); 449 | $this->load->model('extension/module/foc_csv'); 450 | 451 | // first call - generate upload key 452 | $key = $this->model_extension_module_foc_csv->prepareUploadPath(); 453 | $importFile = $this->model_extension_module_foc_csv->getImportCsvFilePath($key); 454 | $imagesFile = $this->model_extension_module_foc_csv->getImportImagesZipPath($key); 455 | 456 | // $profile = json_decode($_POST['profile-json'], true); 457 | $profile = $this->model_extension_module_foc_csv->fillProfileEmptyValues(json_decode($_POST['profile-json'], true)); 458 | $this->model_extension_module_foc_csv->applyProfile($profile); 459 | 460 | // csv file operations 461 | if (isset($_FILES['csv-file'])) { 462 | 463 | $this->model_extension_module_foc_csv->writeLog('CSV file uploaded'); 464 | 465 | $db_charset = strtolower($this->model_extension_module_foc_csv->getDBCharset()); 466 | $charset = $this->model_extension_module_foc_csv->dbToIconvCharset($db_charset); 467 | $encoding = strtolower($profile['encoding']); 468 | 469 | // try change file encoding 470 | if ($encoding !== 'none' && $charset !== $encoding) { 471 | $this->model_extension_module_foc_csv->writeLog('Trying to convert character encoding from [' . $encoding . '] to [' . $charset . ']'); 472 | 473 | if (!function_exists('iconv')) { 474 | $this->model_extension_module_foc_csv->writeLog('Please install iconv or convert csv file to [' . $charset . ']', 'error'); 475 | $this->sendFail('Please install iconv or convert csv file to [' . $charset . ']'); 476 | } 477 | 478 | $src = fopen($_FILES['csv-file']['tmp_name'], 'r'); 479 | $out = fopen($importFile, 'w'); 480 | 481 | while ($line = fgets($src)) { 482 | fwrite($out, iconv($encoding, $charset . '//TRANSLIT//IGNORE', $line)); 483 | } 484 | 485 | fclose($src); 486 | fclose($out); 487 | 488 | $this->model_extension_module_foc_csv->writeLog('File [' . $src . '] encoded successfully!'); 489 | } 490 | else { 491 | move_uploaded_file($_FILES['csv-file']['tmp_name'], $importFile); 492 | } 493 | } 494 | // images zip - save and unpack 495 | if (isset($_FILES['images-zip'])) { 496 | $this->model_extension_module_foc_csv->writeLog('Images ZIP file uploaded'); 497 | 498 | move_uploaded_file($_FILES['images-zip']['tmp_name'], $imagesFile); 499 | 500 | $zip = new ZipArchive(); 501 | $can_open = $zip->open($imagesFile); 502 | 503 | if ($can_open === true) { 504 | $zip->extractTo($this->model_extension_module_foc_csv->getImportImagesPath($key)); 505 | $this->model_extension_module_foc_csv->moveUploadedImages($key); 506 | $zip->close(); 507 | } 508 | } 509 | 510 | // read csv bytes count 511 | $csv_file = new SplFileObject($importFile, 'r'); 512 | $csv_file->seek(PHP_INT_MAX); 513 | $csv_total = $csv_file->ftell(); 514 | 515 | $import_url = $this->createUrl('extension/module/foc_csv/importPart'); 516 | 517 | // remove manufacturers if necessary 518 | if (isset($profile['removeManufacturersBeforeImport']) 519 | && $profile['removeManufacturersBeforeImport'] 520 | ) { 521 | $this->model_extension_module_foc_csv->writeLog('Clearing manufacturers table'); 522 | $this->model_extension_module_foc_csv->clearManufacturers(); 523 | } 524 | 525 | // removeOthers importMode handler: 526 | // before importData we remove all products from database 527 | // on importPart this mode === updateCreate mode 528 | if ($profile['importMode'] == 'removeOthers') { 529 | $this->model_extension_module_foc_csv->clearProducts(); 530 | } 531 | 532 | $this->sendOk(array( 533 | 'csvTotal' => $csv_total, 534 | 'key' => $key, 535 | 'importUrl' => html_entity_decode($import_url), 536 | // position - last ftell file position 537 | 'position' => 0 538 | )); 539 | } 540 | } 541 | 542 | /* 543 | Import partition from CSV 544 | */ 545 | public function importPart ($key = null, $position = null, $profile = null) { 546 | if ($key === null && $position === null && $profile === null) { 547 | if ($this->request->server['REQUEST_METHOD'] == 'POST') { 548 | $json = json_decode(file_get_contents('php://input'), true); 549 | 550 | $this->load->model('extension/module/foc_csv_common'); 551 | $this->load->model('extension/module/foc_csv'); 552 | 553 | if (isset($json['position']) 554 | && isset($json['key']) 555 | && isset($json['profile']) 556 | ) { 557 | $import_key = $json['key']; 558 | $position = $json['position']; 559 | $errors = isset($json['errors']) ? intval($json['errors']) : 0; 560 | $lines = isset($json['lines']) ? $json['lines'] : 0; 561 | $profile = $json['profile']; 562 | 563 | $this->model_extension_module_foc_csv->writeLog('Import part start [' . $lines . ']'); 564 | 565 | $profile = $this->model_extension_module_foc_csv->fillProfileEmptyValues($profile); 566 | $this->model_extension_module_foc_csv->applyProfile($profile); 567 | 568 | $this->model_extension_module_foc_csv->setUploadKey($import_key); 569 | 570 | $skipLines = $profile['skipLines']; 571 | 572 | if (!$profile['csvWithoutHeaders'] && $profile['csvHeadersLineNumber'] > $skipLines) { 573 | $skipLines = $profile['csvHeadersLineNumber']; 574 | } 575 | 576 | $delimiter = empty($profile['csvFieldDelimiter']) ? ';' : $profile['csvFieldDelimiter']; 577 | $importAtOnce = empty($profile['processAtStepNum']) ? 10 : $profile['processAtStepNum']; 578 | 579 | $mode = $profile['importMode']; 580 | $this->model_extension_module_foc_csv->setImportMode($mode); 581 | 582 | $key_field = $profile['keyField']; 583 | list($table, $key) = explode(':', $key_field); 584 | $this->model_extension_module_foc_csv->toggleKeyField($table, $key); 585 | 586 | $path = $this->model_extension_module_foc_csv->getImportCsvFilePath($import_key); 587 | $csv_fid = fopen($path, 'r'); 588 | 589 | if ($position > 0) { 590 | fseek($csv_fid, $position); 591 | } 592 | 593 | $i = 0; 594 | 595 | while ($i < $importAtOnce && ($line = fgetcsv($csv_fid, 0, $delimiter)) !== false) { 596 | $i++; 597 | 598 | if (($lines + $i) <= $skipLines) { 599 | $this->model_extension_module_foc_csv->writeLog('Skip line:' . ($lines + $i) . ', lines to skip:' . $skipLines); 600 | continue; 601 | } 602 | // import stuff.. 603 | try { 604 | if (!$this->model_extension_module_foc_csv->import($profile, $line, $lines + $i)) { 605 | $errors++; 606 | } 607 | } 608 | catch (Exception $e) { 609 | $this->model_extension_module_foc_csv->writeLog('Error on import at [' . ($lines + $i) . '] (' . $e->getMessage() . ')', 'error'); 610 | $this->sendFail($e->getMessage()); 611 | } 612 | } 613 | 614 | $position = ftell($csv_fid); 615 | fclose($csv_fid); 616 | 617 | $this->model_extension_module_foc_csv->writeLog('Import part end at [' . ($lines + $i) . ']'); 618 | 619 | $this->sendOk(array( 620 | 'key' => $import_key, 621 | 'position' => $position, 622 | 'errors' => $errors, 623 | 'lines' => $i + $lines 624 | )); 625 | } 626 | } 627 | } 628 | 629 | $this->model_extension_module_foc_csv->writeLog('Missing required fields! Fields are $key: [' . $key .'], $position: [' . $position . '] and $profile: [' . print_r($profile, true) . ']', 'error'); 630 | $this->sendFail(); 631 | } 632 | 633 | /* 634 | Download CSV or Images.zip 635 | */ 636 | public function download () { 637 | $this->load->model('extension/module/foc_csv_common'); 638 | $this->load->model('extension/module/foc_csv_exporter'); 639 | $key = null; 640 | $mode = 0; 641 | 642 | if (isset($this->request->get['key'])) { 643 | $key = $this->request->get['key']; 644 | } 645 | 646 | if (isset($this->request->get['mode'])) { 647 | $mode = $this->request->get['mode']; 648 | } 649 | 650 | $file_path = null; 651 | 652 | switch ($mode) { 653 | case self::KEY_FOR_CSV_FILE: 654 | $file_path = $this->model_extension_module_foc_csv_exporter->getExportCsvFilePath($key); 655 | break; 656 | case self::KEY_FOR_IMAGES_ZIP_FILE: 657 | $file_path = $this->model_extension_module_foc_csv_exporter->getExportImagesZipFilePath($key); 658 | break; 659 | } 660 | 661 | /* 662 | TODO: 663 | make option for auto remove files after download 664 | */ 665 | if ($file_path !== null && is_file($file_path)) { 666 | header('Content-Type: application/octet-stream'); 667 | header('Content-Description: File Transfer'); 668 | header('Content-Disposition: attachment; filename="' . basename($file_path) . '"'); 669 | header('Content-Transfer-Encoding: binary'); 670 | header('Expires: 0'); 671 | header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); 672 | header('Pragma: public'); 673 | header('Content-Length: ' . filesize($file_path)); 674 | 675 | readfile($file_path, 'rb'); 676 | } 677 | else { 678 | exit('Error: Could not find file ' . $file_path . '!'); 679 | } 680 | } 681 | 682 | /* 683 | Autocomplete to choose attributes default group 684 | */ 685 | public function attributesGroupAutocomplete () { 686 | $this->load->model('catalog/attribute_group'); 687 | $groups = $this->model_catalog_attribute_group->getAttributeGroups(); 688 | $response = array(); 689 | foreach ($groups as $group) { 690 | $response[] = array( 691 | 'name' => $group['name'] 692 | ); 693 | } 694 | 695 | echo json_encode($response); 696 | die; 697 | } 698 | 699 | private function breadcrumbs () { 700 | $breadcrumbs = array(); 701 | 702 | $breadcrumbs[] = array( 703 | 'text' => $this->language->get('text_home'), 704 | 'href' => $this->createUrl('common/home'), 705 | 'separator' => false 706 | ); 707 | $breadcrumbs[] = array( 708 | 'text' => $this->language->get('text_extension'), 709 | 'href' => $this->createUrl('extension/extension'), 710 | 'separator' => ' :: ' 711 | ); 712 | $breadcrumbs[] = array( 713 | 'text' => $this->language->get('heading_title'), 714 | 'href' => $this->createUrl('extension/module/foc_csv'), 715 | 'separator' => ' :: ' 716 | ); 717 | 718 | return $breadcrumbs; 719 | } 720 | 721 | } -------------------------------------------------------------------------------- /upload/admin/language/en-gb/extension/module/foc_attribute_encoders.php: -------------------------------------------------------------------------------- 1 | site, e-mail'; 7 | $_['foc_app_preload_nojs'] = 'Please enable JavaScript to use module!'; 8 | 9 | $_['foc_default_attributes_group'] = 'Specification'; -------------------------------------------------------------------------------- /upload/admin/language/ru-ru/extension/module/foc_attribute_encoders.php: -------------------------------------------------------------------------------- 1 | сайт, e-mail'; 7 | $_['foc_app_preload_nojs'] = 'Пожалуйста, включите JavaScript в вашем браузере!'; 8 | 9 | $_['foc_default_attributes_group'] = 'Характеристики'; -------------------------------------------------------------------------------- /upload/admin/model/extension/module/foc_csv_common.php: -------------------------------------------------------------------------------- 1 | array( 15 | 'sort_order', 16 | 'language_id', 17 | 'date_added', 18 | 'date_modified' 19 | ), 20 | 'product' => array( 21 | 'location', 22 | 'manufacturer_id', 23 | 'shipping', 24 | 'points', 25 | 'tax_class_id', 26 | 'weight_class_id', 27 | 'length_class_id', 28 | 'subtract', 29 | 'minimum' 30 | ), 31 | 'product_special' => array( 32 | 'product_special_id', 33 | 'customer_group_id', 34 | 'product_id', 35 | 'priority' 36 | ), 37 | 'product_description' => array( 38 | 'product_id' 39 | ), 40 | 'product_image' => array( 41 | 'product_id', 42 | 'product_image_id' 43 | ), 44 | 'category_description' => array( 45 | 'category_id' 46 | ), 47 | 'category' => array( 48 | 'parent_id', 49 | 'top', 50 | 'column' 51 | ) 52 | ); 53 | 54 | 55 | // db encoding -> iconv encoding 56 | protected $charsetMap = array( 57 | 'armscii8' => 'ARMSCII-8', 58 | 'ascii' => 'ASCII', 59 | 'big5' => 'BIG-5', 60 | 'binary'=> null, // Not sure what is it 61 | 'cp1250'=> 'CP1250', 62 | 'cp1251'=> 'CP1251', 63 | 'cp1256'=> 'CP1256', 64 | 'cp1257'=> 'CP1257', 65 | 'cp850'=> 'CP850', 66 | 'cp852'=> 'CP852', 67 | 'cp866'=> 'CP866', 68 | 'cp932'=> 'CP932', 69 | 'dec8'=> 'ISO-8859-1', 70 | 'eucjpms'=> 'EUC-JISX0213', 71 | 'euckr'=> 'EUC-KR', 72 | 'gb18030'=> 'GB18030', 73 | 'gb2312'=> 'GB_2312-80', 74 | 'gbk'=> 'GBK', 75 | 'geostd8'=> 'GEORGIAN-PS', 76 | 'greek'=> 'GREEK', 77 | 'hebrew'=> 'HEBREW', 78 | 'hp8'=> 'R8', 79 | 'keybcs2'=> null, // Not sure here 80 | 'koi8r'=> 'KOI8-R', 81 | 'koi8u'=> 'KOI8-U', 82 | 'latin1'=> 'LATIN1', 83 | 'latin2'=> 'LATIN2', 84 | 'latin5'=> 'LATIN5', 85 | 'latin7'=> 'LATIN7', 86 | 'macce'=> 'MACCENTRALEUROPE', 87 | 'macroman'=> 'MACROMANIA', 88 | 'sjis'=> 'SJIS', 89 | 'swe7'=> null, // Not sure here, please make PR if there is error 90 | 'tis620'=> 'TIS-620', 91 | 'ucs2'=> 'UCS-2', 92 | 'ujis'=> 'EUC-JP', 93 | 'utf16'=> 'UTF-16', 94 | 'utf16le'=> 'UTF-16LE', 95 | 'utf32'=> 'UTF-32', 96 | 'utf8mb3'=> 'UTF-8', 97 | 'utf8mb4'=> 'UTF-8', 98 | ); 99 | 100 | public function __construct ($registry) { 101 | parent::__construct($registry); 102 | $this->scanOpencartVersion(); 103 | $this->log = new Log('foc_csv_' . $this->type . '.txt'); 104 | $this->load->library('FocSimpleTemplater'); 105 | } 106 | 107 | public function install () { 108 | $this->load->model('setting/setting'); 109 | $this->model_setting_setting->editSetting($this->profiles_code, array($this->profiles_key => array())); 110 | $this->saveProfiles($this->getDefaultProfiles()); 111 | } 112 | 113 | /* 114 | 115 | */ 116 | public function writeLog ($msg, $group = 'info') { 117 | switch ($group) { 118 | case 'error': $msg = '[ERROR] ' . $msg; 119 | break; 120 | case 'warn' : $msg = '[WARN] ' . $msg; 121 | break; 122 | default : $msg = '[INFO] ' . $msg; 123 | break; 124 | } 125 | 126 | $this->log->write($msg); 127 | } 128 | 129 | /* 130 | Key fields 131 | */ 132 | public function getKeyFields () { 133 | return array( 134 | 'product:product_id', 135 | 'product:sku', 136 | 'product:model', 137 | 'product_description:name', 138 | 'product:ean', 139 | 'product:mpn', 140 | 'product:jan', 141 | 'product:isbn' 142 | ); 143 | } 144 | 145 | /* 146 | Remove unwanted fields from select 147 | */ 148 | public function filterTableFields ($table, array $fields) { 149 | if (isset($this->unwantedTableFields[$table])) { 150 | $fields = array_diff($fields, $this->unwantedTableFields[$table]); 151 | } 152 | 153 | return array_diff($fields, $this->unwantedTableFields['common']); 154 | } 155 | 156 | /* 157 | Generate DB fields list 158 | */ 159 | public function getDbFields () { 160 | $tables = array( 161 | 'product', 162 | 'product_description', 163 | 'product_image', 164 | 'product_special', 165 | 'manufacturer', 166 | 'category', 167 | 'category_description' 168 | ); 169 | 170 | $result = array(); 171 | 172 | foreach ($tables as $table) { 173 | $data = $this->db->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = "' . DB_DATABASE . '" AND TABLE_NAME = "' . DB_PREFIX . $table . '"'); 174 | $result[$table] = array_column($data->rows, 'COLUMN_NAME'); 175 | $result[$table] = $this->filterTableFields($table, $result[$table]); 176 | } 177 | 178 | return $result; 179 | } 180 | 181 | public function fillProfileEmptyValues ($profile) { 182 | return array_replace_recursive($this->getDefaultProfile(), $profile); 183 | } 184 | 185 | public function getDefaultProfile () { 186 | return array(); 187 | } 188 | 189 | /* 190 | Default profiles list 191 | */ 192 | public function getDefaultProfiles () { 193 | return array( 194 | 'default' => $this->getDefaultProfile() 195 | ); 196 | } 197 | 198 | /* 199 | Load all profiles 200 | */ 201 | public function loadProfiles () { 202 | $this->load->model('setting/setting'); 203 | 204 | $profiles = json_decode($this->model_setting_setting->getSettingValue($this->profiles_key), true); 205 | 206 | if (count($profiles) === 0) { 207 | $profiles = $this->getDefaultProfiles(); 208 | } 209 | else { 210 | foreach ($profiles as $key => $profile) { 211 | $profiles[$key] = $this->fillProfileEmptyValues($profile); 212 | } 213 | } 214 | 215 | return $profiles; 216 | } 217 | 218 | /* 219 | Load profile by name 220 | */ 221 | public function loadProfile ($name) { 222 | $profiles = $this->loadProfiles(); 223 | 224 | if (isset($profiles[$name])) { 225 | return $profiles[$name]; 226 | } 227 | 228 | return null; 229 | } 230 | 231 | /* 232 | Save profiles list 233 | */ 234 | public function saveProfiles ($profiles) { 235 | $this->load->model('setting/setting'); 236 | 237 | foreach ($profiles as $key => $profile) { 238 | $profiles[$key] = $this->fillProfileEmptyValues($profile); 239 | } 240 | 241 | $this->model_setting_setting->editSettingValue( 242 | $this->profiles_code, 243 | $this->profiles_key, 244 | $profiles 245 | ); 246 | } 247 | 248 | /* 249 | Save profile by name 250 | */ 251 | public function setProfile ($name, $data) { 252 | $profiles = $this->loadProfiles(); 253 | $profiles[$name] = $data; 254 | $this->saveProfiles($profiles); 255 | } 256 | 257 | /* 258 | FILE MANIPULATION METHODS 259 | */ 260 | 261 | /* 262 | Return path to file by key 263 | */ 264 | public function getUploadPath ($key) { 265 | return DIR_CACHE . $this->profiles_code . DIRECTORY_SEPARATOR . $key . DIRECTORY_SEPARATOR . $this->type . DIRECTORY_SEPARATOR; 266 | } 267 | 268 | /* 269 | Prepare import storage 270 | Returns storage key 271 | */ 272 | public function prepareUploadPath () { 273 | $key = md5(rand() . time()); 274 | $path = $this->getUploadPath($key); 275 | 276 | if (is_dir($path)) { 277 | return $this->prepareUploadPath(); 278 | } 279 | 280 | $this->writeLog('Trying to create import path [' . $path . ']'); 281 | mkdir($path, 0755, true); 282 | 283 | $this->setUploadKey($key); 284 | 285 | return $key; 286 | } 287 | 288 | public function setUploadKey ($key) { 289 | $this->uploadKey = $key; 290 | } 291 | 292 | /* UTILS */ 293 | 294 | /* 295 | Set state data from frontend profile 296 | */ 297 | public function applyProfile ($profile) { 298 | $this->language_id = (int) $this->config->get('config_language_id'); 299 | if (isset($profile['language'])) { 300 | $this->language_id = (int) $profile['language']; 301 | } 302 | 303 | $this->store_id = $this->config->get('config_store_id'); 304 | if (isset($profile['store'])) { 305 | $this->store_id = $profile['store']; 306 | } 307 | } 308 | /* 309 | Convert FS path to URL 310 | */ 311 | public function pathToUrl ($path) { 312 | return str_replace(DIR_CACHE, HTTPS_CATALOG . 'system/storage/cache/', $path); 313 | } 314 | 315 | public function basename ($path) { 316 | $parts = explode(DIRECTORY_SEPARATOR, $path); 317 | return end($parts); 318 | } 319 | 320 | /* 321 | Check if url is url:) 322 | */ 323 | public function isUrl ($url) { 324 | return preg_match('/^https?\:\/\//', $url); 325 | } 326 | 327 | /* 328 | Get database charset 329 | */ 330 | public function getDBCharset () { 331 | return $this->db->query('SELECT @@character_set_database AS `charset`')->row['charset']; 332 | } 333 | 334 | public function dbToIconvCharset ($charset) { 335 | if (isset($this->charsetMap[$charset])) { 336 | return $this->charsetMap[$charset]; 337 | } 338 | return $charset; 339 | } 340 | 341 | public function getLanguageCode () { 342 | $lang = $this->language->get('code'); 343 | return strtolower(substr($lang, 0, 2)); 344 | } 345 | 346 | /* 347 | Scan version 348 | */ 349 | public function scanOpencartVersion () { 350 | $digits = explode('.', VERSION); 351 | 352 | // ocstore uses 5-digits versions 353 | if (count($digits) > 4) { 354 | $this->is_ocstore = true; 355 | array_pop($digits); 356 | } 357 | 358 | $this->normalized_version = intval(implode('', $digits)); 359 | } 360 | 361 | /* 362 | Version checkers 363 | */ 364 | public function getOpencartMajorVersion () { 365 | return floor($this->normalized_version / 1000); 366 | } 367 | public function isOpencart15 () { 368 | return $this->normalized_version < 2000; 369 | } 370 | 371 | public function isOpencart2 () { 372 | return $this->normalized_version >= 2000 && $this->normalized_version < 2300; 373 | } 374 | public function isOpencart23 () { 375 | return $this->normalized_version >= 2300 && $this->normalized_version < 3000; 376 | } 377 | 378 | public function isOpencart3 () { 379 | return $this->normalized_version >= 3000; 380 | } 381 | 382 | /* 383 | Check is this a ocStore 384 | */ 385 | public function isOcstore () { 386 | return $this->is_ocstore; 387 | } 388 | 389 | } 390 | -------------------------------------------------------------------------------- /upload/admin/view/javascript/.gitignore: -------------------------------------------------------------------------------- 1 | foc_csv/css 2 | foc_csv/js 3 | -------------------------------------------------------------------------------- /upload/admin/view/template/extension/module/foc_csv.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 19 | 20 |
21 |
22 |
23 |
24 | 33 |
34 |
35 |
36 | : 37 |
38 |
39 |

40 | 41 |

42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /upload/admin/view/template/extension/module/foc_csv.twig: -------------------------------------------------------------------------------- 1 | {{ header }} 2 | {{ column_left }} 3 | 4 |
5 | 6 | 19 | 20 |
21 |
22 |
23 |
24 | 33 |
34 |
35 |
36 | {{ heading_title }}: {{ foc_version }} 37 |
38 |
39 |

40 | {{ foc_app_preload_description }} 41 |

42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 | 66 | {% for script in scripts %} 67 | 68 | {% endfor %} 69 | 70 | {{ footer }} -------------------------------------------------------------------------------- /upload/system/library/FocSimpleTemplater.php: -------------------------------------------------------------------------------- 1 | load->library('FocSimpleTemplater'); 9 | // after that, you can call FinSimpleTemplater directly: 10 | FinSimpleTemplater::render($tpl, array()) 11 | 12 | 13 | Templates language: 14 | 15 | Variable interpolation: 16 | {{ variable_interpolation }} 17 | 18 | Loops: 19 | [@each (value, index) <= source] 20 | loop content here 21 | {{ loop.index }} or {{ index }} <- current iteration index (starting from 1) 22 | {{ value.name }} 23 | [@endeach] 24 | 25 | [@each value <= source] 26 | {{ value }} 27 | [@endeach] 28 | 29 | Functions (see $enabled_functions to list available): 30 | [@fn FUNCTION_NAME | ARGUMENT] 31 | 32 | [@date | 'Y-m-d'] - date('Y-m-d') 33 | [@md5 | @variable] - md5($variables['variable']) 34 | */ 35 | class FocSimpleTemplater { 36 | 37 | // available functions 38 | protected static $enabled_functions = array( 39 | 'nl2br', 40 | 'date', 41 | 'htmlspecialchars', 42 | 'htmlentities', 43 | 'html_entity_decode', 44 | 'md5', 45 | 'lcfirst', 46 | 'ucfirst', 47 | 'strtolower', 48 | 'strtoupper', 49 | 'ucwords', 50 | 'trim', 51 | 'time', 52 | 'microtime', 53 | 'money_format', 54 | 'number_format' 55 | ); 56 | 57 | // multiline to single line 58 | protected static function normalize ($template) { 59 | return trim(preg_replace("/\r?\n/", "", preg_replace("/[^\S\t]+/", " ", $template))); 60 | } 61 | 62 | // make arguments array from string 63 | protected static function process_function_args ($args_raw, $vars = array()) { 64 | $args = array_map('trim', preg_split('/,/', $args_raw, -1, PREG_SPLIT_NO_EMPTY)); 65 | $result = array(); 66 | 67 | foreach ($args as $arg) { 68 | if (preg_match("/^\@(.*)/", $arg, $matches)) { 69 | $result[] = isset($vars[$matches[1]]) ? $vars[$matches[1]] : null; 70 | } 71 | else { 72 | $result[] = trim($arg, "\"\'"); 73 | } 74 | } 75 | 76 | return $result; 77 | } 78 | 79 | // parse and execute function code 80 | protected static function execute_function_code ($code, $vars = array()) { 81 | $result = ''; 82 | 83 | $f_name = null; 84 | $f_args_raw = ''; 85 | 86 | $parts = array_map('trim', explode('|', $code)); 87 | 88 | if (count($parts) > 1) { 89 | list ($f_name, $f_args_raw) = $parts; 90 | } 91 | else { 92 | $f_name = $parts[0]; 93 | } 94 | 95 | $f_args = self::process_function_args($f_args_raw, $vars); 96 | 97 | if (in_array($f_name, self::$enabled_functions)) { 98 | $result = call_user_func_array($f_name, $f_args); 99 | } 100 | 101 | return $result; 102 | } 103 | 104 | /* 105 | render functions: 106 | [@fn | ] 107 | */ 108 | public static function render_functions ($template, $vars = array()) { 109 | return preg_replace_callback( 110 | "/\[\@fn ([^\]]+)\]/ium", 111 | function ($matches) use ($vars) { 112 | if (count($matches) === 2) { 113 | $fn = $matches[1]; 114 | return self::execute_function_code($fn, $vars); 115 | } 116 | return ''; 117 | }, $template); 118 | } 119 | 120 | // replace variables with values 121 | public static function render_vars ($template, $vars = array()) { 122 | $result = $template; 123 | 124 | foreach ($vars as $name => $value) { 125 | $replacement = $value; 126 | // we do not support nested loops, so just replace with json string 127 | if (is_array($replacement)) { 128 | $replacement = '[' . json_encode($replacement) . ']'; 129 | } 130 | 131 | $result = preg_replace('/{{ ' . preg_quote($name) . ' }}/', $replacement, $result); 132 | } 133 | return $result; 134 | } 135 | 136 | // render loop 137 | public static function render_loop ($loop_cond, $loop_body, $data = array()) { 138 | $result = ''; 139 | list($condition, $source_name) = explode('<=', $loop_cond); 140 | $loop_vars = explode(',', str_replace(array('(', ')', ' '), '', $condition)); 141 | 142 | if (count($loop_vars) > 1) { 143 | list($l_value, $l_key) = $loop_vars; 144 | } 145 | else { 146 | $l_value = $loop_vars[0]; 147 | $l_key = 'loop.index'; 148 | } 149 | 150 | $source_name = trim($source_name); 151 | 152 | if (!isset($data[$source_name]) || empty($data[$source_name])) { 153 | return $result; 154 | } 155 | 156 | $index = 1; 157 | foreach ($data[$source_name] as $key => $value) { 158 | $local_vars = $data; 159 | $local_vars[$l_key] = $index++; 160 | 161 | $local_vars[$l_value] = $value; 162 | 163 | if (!is_numeric($key)) { 164 | $local_vars[$l_value . '.' . $key] = $value; 165 | } 166 | else { 167 | if (is_array($value)) { 168 | foreach ($value as $attrName => $attrValue) { 169 | $local_vars[$l_value . '.' . $attrName] = $attrValue; 170 | } 171 | } 172 | } 173 | $result .= self::render_vars($loop_body, $local_vars); 174 | } 175 | 176 | return self::render_functions($result, $local_vars); 177 | } 178 | 179 | // render template 180 | public static function render ($template, $vars = array()) { 181 | /* 182 | 183 | [@each (field,iter) <= source] 184 | values 185 | [@endeach] 186 | something other 187 | 188 | becomes single line 189 | */ 190 | $normalized_template = self::normalize($template); 191 | $loops = array_filter(explode('[@endeach]', $normalized_template)); 192 | 193 | $result = ''; 194 | 195 | /* 196 |
[@each (field,iter) <= source]values 197 | processed as: 198 | 0: whole match 199 | 1: pre:
200 | 2: loop_cond: (field,iter) <= source 201 | 3: loop_body: values 202 | */ 203 | foreach ($loops as $loop) { 204 | if (preg_match("/((?!\[\@each).*)?\[\@each ([^\]]+)\]\s*(.*)/iu", $loop, $matches)) { 205 | if (count($matches) === 4) { 206 | $pre = $matches[1]; 207 | $loop_cond = $matches[2]; 208 | $loop_body = $matches[3]; 209 | $pre = trim(self::render_vars($pre, $vars)); 210 | $result .= trim(self::render_functions($pre, $vars)); 211 | $result .= trim(self::render_loop($loop_cond, $loop_body, $vars)); 212 | } 213 | } 214 | else { 215 | $fns_executed = self::render_functions($loop, $vars); 216 | $result .= trim(self::render_vars($fns_executed, $vars)); 217 | } 218 | } 219 | return $result; 220 | } 221 | } --------------------------------------------------------------------------------