UNPKG

@shagital/adonisjs-crud-generator

Version:

Adonisjs Admin Panel Generator is a package that helps you quickly scaffold your typical CRUD admin interfaces. It generates the admin panel code based on the existing (migrated) table in the database

352 lines (346 loc) 15.7 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const standalone_1 = require("@adonisjs/core/build/standalone"); const helpers_1 = require("../common/helpers"); const fs_extra_1 = __importDefault(require("fs-extra")); const pluralize_1 = __importDefault(require("pluralize")); class ViewGenerator extends standalone_1.BaseCommand { async run() { this.Env = this.application.container.use('Adonis/Core/Env'); const dbInstance = this.application.container.use('Adonis/Lucid/Database'); if (this.connection) { (0, helpers_1.validateConnection)(dbInstance, this.application.config, this.connection); } const tableColumns = (0, helpers_1.getTableColumnsAndTypes)(dbInstance, this.application.config); let tableName = this.table.toLowerCase(); let columnsTypes = await tableColumns(tableName, this.connection); let singular = pluralize_1.default.singular(tableName); let plural = pluralize_1.default.plural(tableName); let snakeCase = (0, helpers_1.toSnakeCase)(tableName); this.columns = columnsTypes; let pascalName = (0, helpers_1.pascalCase)(singular); let pascalPlural = pluralize_1.default.plural(pascalName); Promise.all([ this.copyAndUpdateViews({ pascalPlural, pascalName, singular, snakeCase }), this.updateRoutes({ plural, pascalName, pascalPlural }), this.copyAndUpdateStore({ singular, plural, pascalName }), this.updateStoreImport({ singular }), this.updateSidebar({ pascalPlural, plural }), this.updateTranslations({ snakeCase, pascalName, pascalPlural }), ]).then(() => { this.logger.info('Done'); process.exit(); }).catch(e => { console.log(e); }); } copyAndUpdateViews({ pascalPlural, pascalName, singular, snakeCase }) { this.logger.info('Setting up view file'); // copy views fs_extra_1.default.copySync(`${__dirname}/../templates/views`, this.application.viewsPath(`admin/src/views/${pascalPlural}`)); // update views let paths = [ this.application.viewsPath(`admin/src/views/${pascalPlural}/Layout.vue`), this.application.viewsPath(`admin/src/views/${pascalPlural}/Form.vue`), this.application.viewsPath(`admin/src/views/${pascalPlural}/Index.vue`), this.application.viewsPath(`admin/src/views/${pascalPlural}/Filter.vue`), ]; let inputFieldsMarker = ` <!-- insert input fields here. Do not remove -->`; let columnsMarker = `// Insert columns here. Do not remove`; for (let path of paths) { this.updateView({ path, columnsMarker, inputFieldsMarker, pascalName, singular, pascalPlural, snakeCase, }); } } updateView({ path, columnsMarker, inputFieldsMarker, pascalName, singular, pascalPlural, snakeCase, }) { let data = String(fs_extra_1.default.readFileSync(path)); let columns = this.generateVueTableColumns(snakeCase); let inputFields = this.generateInputFields(snakeCase); data = data .replace(new RegExp('{{pascalName}}', 'g'), pascalName) .replace(new RegExp('{{singular}}', 'g'), singular) .replace(new RegExp('{{pascalPlural}}', 'g'), pascalPlural) .replace(new RegExp('{{snakeCase}}', 'g'), snakeCase); if (!data.includes(inputFields)) { data = data.replace(inputFieldsMarker, inputFields); } if (!data.includes(columns)) { data = data.replace(columnsMarker, columns); } fs_extra_1.default.writeFileSync(path, data); } updateRoutes({ pascalName, plural, pascalPlural }) { this.logger.info('Setting up routes'); // update route let importMarker = `// Import Model Components Here. Do not remove this line`; let routeMarker = `// Import Model Routes Here. Do not remove this line`; let routePath = this.application.viewsPath(`admin/src/router/index.js`); let data = String(fs_extra_1.default.readFileSync(routePath)); let imports = ` // ${pascalPlural} import ${pascalName} from "@/views/${pascalPlural}/Layout.vue"; import ${pascalName}Index from "@/views/${pascalPlural}/Index.vue"; import ${pascalName}Form from "@/views/${pascalPlural}/Form.vue"; `; let routes = ` { path: "/${plural}", name: "${pascalPlural}", redirect: "/${plural}/index", components: {default:${pascalName}}, meta: {auth: true}, children: [ { path: "index", name: "${pascalPlural}Index", component: ${pascalName}Index, }, { path: "create", name: "${pascalPlural}New", component: ${pascalName}Form, }, { path: ":id", name: "${pascalPlural}Edit", component: ${pascalName}Form, }, ] },`; if (!data.includes(imports + importMarker)) { data = data.replace(importMarker, imports + importMarker); } if (!data.includes(routes + routeMarker)) { data = data.replace(routeMarker, routes + routeMarker); } fs_extra_1.default.writeFileSync(routePath, data); } copyAndUpdateStore({ singular, plural, pascalName }) { this.logger.info('Setting up vuex store'); // copy store fs_extra_1.default.copySync(`${__dirname}/../templates/store`, this.application.viewsPath(`admin/src/store/${singular}`)); // update store let paths = [ this.application.viewsPath(`admin/src/store/${singular}/actions.js`), this.application.viewsPath(`admin/src/store/${singular}/getters.js`), this.application.viewsPath(`admin/src/store/${singular}/mutations.js`), this.application.viewsPath(`admin/src/store/${singular}/store.js`), ]; const baseUrl = `${this.Env.get('APP_URL')}/${this.application.config.get('crudGenerator.admin_prefix')}`; for (let path of paths) { let data = fs_extra_1.default.readFileSync(path, 'utf8'); data = data .replace(new RegExp(`{{baseUrl}}`, 'g'), baseUrl) .replace(new RegExp(`{{plural}}`, 'g'), plural) .replace(new RegExp(`{{singular}}`, 'g'), singular) .replace(new RegExp(`{{pascalCapital}}`, 'g'), pascalName.toUpperCase()); fs_extra_1.default.writeFileSync(path, data, 'utf8'); } } updateStoreImport({ singular }) { this.logger.info('Completing store setup'); // update store import let storePath = this.application.viewsPath(`admin/src/store/index.js`); let storeImportMarker = `// Add new store. Don't remove this line`; let storeExportMarker = `// export new store. Don't remove this line`; let data = String(fs_extra_1.default.readFileSync(storePath)); if (!data.includes(`import {${singular}} from './${singular}/store';\n` + storeImportMarker)) { data = data.replace(storeImportMarker, `import {${singular}} from './${singular}/store';\n` + storeImportMarker); } if (!data.includes(`${singular},\n` + storeExportMarker)) { data = data.replace(storeExportMarker, `${singular},\n` + storeExportMarker); } fs_extra_1.default.writeFileSync(storePath, data); } updateTranslations({ snakeCase, pascalName, pascalPlural }) { this.logger.info('Updating translations'); // update store import let translationPath = this.application.viewsPath(`admin/src/locales/en.json`); let jsonString = String(fs_extra_1.default.readFileSync(translationPath)); let data = JSON.parse(jsonString); let jsonData = { name: pascalName, title: pascalName, plural: pascalPlural, column: {}, }; let column = {}; Object.keys(this.columns).forEach((columnName) => { column[(0, helpers_1.toSnakeCase)(columnName)] = (0, helpers_1.titleCase)(columnName); }); jsonData.column = column; if (!data[snakeCase]) { data[snakeCase] = jsonData; } else { let existingColumns = data[snakeCase].column || {}; jsonData.column = { ...existingColumns, ...jsonData.column }; data[snakeCase] = { ...data[snakeCase], ...jsonData }; } jsonString = JSON.stringify(data); fs_extra_1.default.writeFileSync(translationPath, jsonString); } async updateSidebar({ plural, pascalPlural }) { this.logger.info('Adding links to sidebar'); // update store import let path = this.application.viewsPath(`admin/src/components/Sidebar.vue`); let marker = `<!-- INSERT NEW ENDPOINTS HERE. DO NOT REMOVE THIS LINE -->`; let routeString = ` <div :class="\`items-center duration-200 mt-4 py-2 px-6 border-l-4 \${$route.path.includes('${plural}') ? activeClass : inactiveClass}\`" @click="toggleExpandMenu('${plural}')" > <i class="icon-expand-alt"></i> ${pascalPlural} <ul v-if="expandedMenu == '${plural}'" class="p-3"> <li class="p-2"><router-link :class="$route.name == '${pascalPlural}Index' ? activeChildClass : inactiveChildClass" :to="{name: '${pascalPlural}Index'}" > <i class="icon-list"></i>Index </router-link></li> <li class="p-2"> <router-link :class="$route.name == '${pascalPlural}New' ? activeChildClass : inactiveChildClass" :to="{name: '${pascalPlural}New'}" > <i class="icon-plus"></i>Create </router-link> </li> </ul> </div> `; let data = String(fs_extra_1.default.readFileSync(path)); if (!data.includes(routeString + marker)) { data = data.replace(marker, routeString + marker); } fs_extra_1.default.writeFileSync(path, data); } generateInputFields(snakeCase) { let columns = this.columns; let string = ''; Object.keys(columns).forEach((columnName) => { if (!columns[columnName].primary && !['created_at', 'updated_at', 'deleted_at'].includes(columnName)) { let column = columns[columnName]; let validationRules = column.nullable ? '' : 'required|'; let columnType; switch (column['type']) { case 'string': columnType = column.length > 100 ? 'textarea' : 'text'; validationRules += column.length > 0 ? `|max:${column.length}` : ''; break; case 'number': columnType = 'number'; validationRules += `decimal`; validationRules += column.length > 0 ? `|max:${column.length}` : ''; break; case 'datetime': columnType = 'datetime-local'; break; case 'date': columnType = 'date'; break; case 'boolean': columnType = 'checkbox'; break; default: columnType = columnName.includes('url') ? 'url' : 'text'; } // if column name seem like email/password if (columnName.includes('email')) { columnType = 'email'; validationRules += `|email`; } if (columnName.includes('password')) { columnType = 'password'; validationRules += `|min:8`; } validationRules = validationRules.slice(-1) === '|' ? validationRules.slice(0, -1) : validationRules; validationRules = validationRules.charAt(0) === '|' ? validationRules.substring(1) : validationRules; string += ` <div > <label class="text-gray-700" for="${columnName}"><b>{{$t('${snakeCase}.column.${columnName}')}}</b> </label> <${columnType === 'textarea' ? 'textarea' : 'input'} ${column.length > 0 ? `max="${column.length}" step="any"` : ''} name="${columnName}" v-validate="'${validationRules}'" v-model="body.${columnName}" class="${columnType === 'checkbox' ? '' : 'form-input w-full mt-2 rounded-md focus:border-indigo-600'}" type="${columnType}"> ${columnType === 'checkbox' ? `<hr>` : columnType === 'textarea' ? '</textarea>' : ''} <small class="text-red-800" v-show="errors.has('${columnName}')" >{{errors.first('${columnName}')}}</small> </div> `; } }); return string; } generateVueTableColumns(snakeCase) { let columns = this.columns; let string = ''; Object.keys(columns).forEach((columnName) => { let column = columns[columnName]; string += ` { name: "${columnName}", title: () => { return this.$t('${snakeCase}.column.${(0, helpers_1.toSnakeCase)(columnName)}')}, active: true, ${['number', 'datetime', 'date'].includes(column.type) ? `sortField: '${columnName}',` : ``} ${column.type === 'string' && column.length > 100 ? ` callback: s => { return truncate(s) } ` : ``} }, `; }); return string; } } /** * Command name is used to run the command */ ViewGenerator.commandName = 'crud:view'; /** * Command description is displayed in the "help" output */ ViewGenerator.description = 'Generate view templates for a table'; ViewGenerator.settings = { /** * Set the following value to true, if you want to load the application * before running the command */ loadApp: true, /** * Set the following value to true, if you want this command to keep running until * you manually decide to exit the process */ stayAlive: false, }; __decorate([ standalone_1.args.string({ description: 'Table to generate model and relationships for' }), __metadata("design:type", String) ], ViewGenerator.prototype, "table", void 0); __decorate([ standalone_1.flags.boolean({ description: 'Specify custom DB connection to use' }), __metadata("design:type", String) ], ViewGenerator.prototype, "connection", void 0); exports.default = ViewGenerator;