├── .tool-versions ├── .babelrc ├── .prettierrc ├── next.config.js ├── public ├── rh.png ├── favicon.ico └── rh.svg ├── postcss.config.js ├── config └── jest │ └── cssTransform.js ├── pages ├── api │ └── hello.js ├── _app.js ├── _document.js ├── index.js └── g │ ├── model.jsx │ └── migration.jsx ├── .vscode ├── settings.json └── launch.json ├── helpers ├── constants.js ├── FieldTypeSplitter.js ├── ga │ └── index.js ├── FieldTypeSplitter.test.js ├── parseMigrationFormat.js └── parseMigrationFormat.test.js ├── styles ├── globals.css └── Home.module.css ├── setupTests.js ├── tailwind.config.js ├── .gitignore ├── layout ├── Header.jsx └── Footer.jsx ├── components ├── ReferencesConfig.jsx ├── LimitConfig.jsx ├── CopyButton.test.jsx ├── ReferencesConfig.test.jsx ├── MigrationEditor │ ├── CustomMigrationForm.jsx │ ├── JoinTableForm.jsx │ ├── AddColumnsForm.jsx │ ├── RemoveColumnsForm.jsx │ ├── index.jsx │ └── index.test.jsx ├── CopyButton.jsx ├── FieldTypeInput.test.jsx ├── LimitConfig.test.jsx ├── FieldInput.test.jsx ├── PrecisionScaleConfig.test.jsx ├── PrecisionScaleConfig.jsx ├── Heading.jsx ├── Pill.jsx ├── JoinTableEditor │ └── index.jsx ├── FieldInput.jsx └── FieldTypeInput.jsx ├── README.md ├── .github └── workflows │ ├── pull_request.yml │ └── main.yml ├── package.json ├── __tests__ └── pages │ ├── migration.test.jsx │ └── model.test.jsx └── jest.config.js /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 16.5.0 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"] 3 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | basePath: '/railshelp', 3 | } 4 | -------------------------------------------------------------------------------- /public/rh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabigeek/railshelp/HEAD/public/rh.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasabigeek/railshelp/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | process() { 3 | return 'module.exports = {};' 4 | }, 5 | getCacheKey() { 6 | return 'cssTransform' 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default (req, res) => { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "search.exclude": { 5 | "**/.next": true, 6 | "**/out": true 7 | } 8 | } -------------------------------------------------------------------------------- /helpers/constants.js: -------------------------------------------------------------------------------- 1 | export const MIGRATION_FORMATS = { 2 | ADD_COLUMNS: 'AddColumnsToTable', 3 | REMOVE_COLUMNS: 'RemoveColumnsFromTable', 4 | JOIN_TABLE: 'CreateModelJoinTable', 5 | CUSTOM: 'Custom Migration' 6 | } 7 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | 4 | input.text-input { 5 | @apply mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 sm:text-sm; 6 | } 7 | 8 | @tailwind utilities; 9 | -------------------------------------------------------------------------------- /helpers/FieldTypeSplitter.js: -------------------------------------------------------------------------------- 1 | export default class FieldTypeSplitter { 2 | constructor({ text }) { 3 | this.text = text; 4 | } 5 | 6 | split() { 7 | const [type, ...configArray] = this.text.split(/({[a-z0-9|,]+})/); 8 | const config = configArray[0] ?? ""; 9 | return [type, config]; 10 | } 11 | } -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | // optional: configure or set up a testing framework before each test 2 | // if you delete this file, remove `setupFilesAfterEnv` from `jest.config.js` 3 | 4 | // used for __tests__/testing-library.js 5 | // learn more: https://github.com/testing-library/jest-dom 6 | import '@testing-library/jest-dom/extend-expect' 7 | -------------------------------------------------------------------------------- /helpers/ga/index.js: -------------------------------------------------------------------------------- 1 | // log the pageview with their URL 2 | export const pageview = (url) => { 3 | window.gtag('config', process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS, { 4 | page_path: url, 5 | }) 6 | } 7 | 8 | // log specific events happening. 9 | export const event = ({ action, params }) => { 10 | window.gtag('event', action, params) 11 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | module.exports = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx}', 6 | './layout/**/*.{js,ts,jsx,tsx}', 7 | './components/**/*.{js,ts,jsx,tsx}', 8 | ], 9 | darkMode: false, // or 'media' or 'class' 10 | theme: { 11 | extend: { 12 | fontFamily: { 13 | sans: ['Inter var', ...defaultTheme.fontFamily.sans], 14 | }, 15 | }, 16 | }, 17 | plugins: [ 18 | require('@tailwindcss/forms'), 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /layout/Header.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Header = () => ( 4 |
5 |
6 |
7 |
8 |

9 | 10 | rails.help 11 | 12 |

13 |
14 |
15 |
16 |
17 | ); 18 | 19 | export default Header; 20 | -------------------------------------------------------------------------------- /components/ReferencesConfig.jsx: -------------------------------------------------------------------------------- 1 | const ReferencesConfig = ({ value, onChange }) => { 2 | return ( 3 |
4 | onChange(e.target.checked ? "{polymorphic}" : "")} 9 | /> 10 | 16 |
17 | ); 18 | }; 19 | 20 | export default ReferencesConfig; 21 | -------------------------------------------------------------------------------- /components/LimitConfig.jsx: -------------------------------------------------------------------------------- 1 | const LimitConfig = ({ value, onChange }) => { 2 | return ( 3 |
4 | 10 | onChange(e.target.value ? `{${e.target.value}}` : "")} 18 | /> 19 |
20 | ); 21 | }; 22 | 23 | export default LimitConfig; 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | I wanted to create a resource on Rails generators that was more comprehensive and interactive than 4 | a cheatsheet. A GUI makes required arguments obvious, and exposes some lesser known options (e.g. `{polymorphic}`). 5 | 6 | ## References 7 | 8 | - [Model Generator README](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/model/USAGE) 9 | 10 | # Development 11 | 12 | Some VSCode debug configurations are provided in `.vscode`. I'm not sure why breakpoints don't seem to work very well, but adding a `debugger` expression works. 13 | 14 | The project runs on [Next.js](https://nextjs.org/docs) and was built with `yarn`. 15 | -------------------------------------------------------------------------------- /components/CopyButton.test.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { render, screen } from "@testing-library/react"; 6 | import userEvent from "@testing-library/user-event"; 7 | import CopyButton from "./CopyButton"; 8 | 9 | it("copies command", async () => { 10 | Object.assign(navigator, { 11 | clipboard: { 12 | writeText: () => {}, 13 | }, 14 | }); 15 | jest.spyOn(navigator.clipboard, "writeText"); 16 | 17 | render(); 18 | const copyButton = screen.getByText("Copy"); 19 | userEvent.click(copyButton); 20 | expect(navigator.clipboard.writeText).toHaveBeenCalledWith("Sample Text"); 21 | }); 22 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: pull request 4 | 5 | # Controls when the action will run. 6 | on: 7 | pull_request: 8 | branches: [main] 9 | 10 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 11 | jobs: 12 | test: 13 | # The type of runner that the job will run on 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Setup Node.js environment 20 | uses: actions/setup-node@v2.1.4 21 | with: 22 | node-version: 12.13.0 23 | 24 | - name: Install JS dependencies 25 | run: yarn install 26 | 27 | - name: Install JS dependencies 28 | run: yarn test 29 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import { useEffect } from 'react' 3 | import { useRouter } from 'next/router' 4 | 5 | import * as ga from '../helpers/ga' 6 | 7 | function MyApp({ Component, pageProps }) { 8 | const router = useRouter() 9 | 10 | useEffect(() => { 11 | const handleRouteChange = (url) => { 12 | ga.pageview(url) 13 | } 14 | //When the component is mounted, subscribe to router changes 15 | //and log those page views 16 | router.events.on('routeChangeComplete', handleRouteChange) 17 | 18 | // If the component is unmounted, unsubscribe 19 | // from the event with the `off` method 20 | return () => { 21 | router.events.off('routeChangeComplete', handleRouteChange) 22 | } 23 | }, [router.events]) 24 | 25 | return 26 | } 27 | 28 | export default MyApp 29 | -------------------------------------------------------------------------------- /components/ReferencesConfig.test.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { render, screen, fireEvent } from '@testing-library/react' 6 | import ReferencesConfig from './ReferencesConfig' 7 | 8 | 9 | it('is not checked when empty value', async () => { 10 | render() 11 | 12 | const checkbox = screen.getByRole('checkbox') 13 | expect(checkbox).not.toBeChecked() 14 | }) 15 | 16 | it('is checked when value is {polymorphic}', async () => { 17 | render() 18 | 19 | const checkbox = screen.getByRole('checkbox') 20 | expect(checkbox).toBeChecked() 21 | }) 22 | 23 | it('calls onChange when clicked', async () => { 24 | const mockOnChange = jest.fn(); 25 | render() 26 | 27 | const checkbox = screen.getByRole('checkbox') 28 | fireEvent.click(checkbox) 29 | 30 | expect(mockOnChange).toHaveBeenCalled(); 31 | }) 32 | -------------------------------------------------------------------------------- /helpers/FieldTypeSplitter.test.js: -------------------------------------------------------------------------------- 1 | import FieldTypeSplitter from './FieldTypeSplitter'; 2 | 3 | describe('split()', () => { 4 | it('returns empty strings if input is empty string', () => { 5 | const splitter = new FieldTypeSplitter({ text: '' }); 6 | 7 | expect(splitter.split()).toEqual(['', '']); 8 | }); 9 | it('returns type if there is no config', () => { 10 | const splitter = new FieldTypeSplitter({ text: 'references' }); 11 | 12 | expect(splitter.split()).toEqual(['references', '']); 13 | }); 14 | it('returns type and single config', () => { 15 | const splitter = new FieldTypeSplitter({ text: 'references{polymorphic}' }); 16 | 17 | expect(splitter.split()).toEqual(['references', '{polymorphic}']); 18 | }); 19 | it('returns type and multiple configs', () => { 20 | const splitter = new FieldTypeSplitter({ text: 'decimal{10,2}' }); 21 | 22 | expect(splitter.split()).toEqual(['decimal', '{10,2}']); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "railshelp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "devDebug": "NODE_OPTIONS='--inspect' next dev", 8 | "build": "next build && next export", 9 | "start": "next start", 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "@headlessui/react": "^1.4.3", 14 | "@heroicons/react": "^1.0.5", 15 | "next": "11", 16 | "react": "17.0.1", 17 | "react-dom": "17.0.1", 18 | "react-use": "^17.1.1" 19 | }, 20 | "devDependencies": { 21 | "@tailwindcss/forms": "^0.4.0", 22 | "@testing-library/jest-dom": "^5.11.9", 23 | "@testing-library/react": "^11.2.5", 24 | "@testing-library/user-event": "^12.7.3", 25 | "autoprefixer": "^10.4.2", 26 | "babel-jest": "^26.6.3", 27 | "identity-obj-proxy": "^3.0.0", 28 | "jest": "^26.6.3", 29 | "postcss": "^8.4.5", 30 | "react-test-renderer": "^17.0.1", 31 | "tailwindcss": "^3.0.15" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | {/* Global Site Tag (gtag.js) - Google Analytics */} 9 |