Giter Site home page Giter Site logo

torch's Introduction

License Hex.pm Build Status Coverage Status

Torch

This version of Torch (5.x) only supports Phoenix 1.7 and above and is not fully backwards-compatible with previous versions of Torch. See UPGRADING for more details.

See v4.0 if you need support for Phoenix 1.6

See v3.0 if you need support for Phoenix 1.5 and below

Torch is a rapid admin generator for Phoenix applications. It creates custom templates and relies on the Phoenix HTML generator under the hood.

image

Requirements

Upgrading

If you are upgrading from Torch v4 (or earlier) you can find additional documentation in the UPGRADING file.

Installation

To install Torch, perform the following steps:

  1. Add torch to your list of dependencies in mix.exs. Then, run mix deps.get:
def deps do
  [
    {:torch, "~> 5.1"}
  ]
end
  1. Add a Plug.Static plug to your endpoint.ex:
plug(
  Plug.Static,
  at: "/torch",
  from: {:torch, "priv/static"},
  gzip: true,
  cache_control_for_etags: "public, max-age=86400",
  headers: [{"access-control-allow-origin", "*"}]
)
  1. Configure Torch by adding the following to your config.exs.
config :torch,
  otp_app: :my_app_name
  1. Run mix torch.install

Now you're ready to start generating your admin! ๐ŸŽ‰

Usage

Torch uses Phoenix generators under the hood. Torch injects it's own custom templates into your priv/static directory, then runs the mix phx.gen.html task with the options you passed in. Finally, it uninstalls the custom templates so they don't interfere with running the plain Phoenix generators.

In light of that fact, the torch.gen.html task takes all the same arguments as the phx.gen.html, but does some extra configuration on either end. Checkout mix help phx.gen.html for more details about the supported options and format.

For example, if we wanted to generate a blog with a Post model we could run the following command:

# mix torch.gen.html <Context Module> <Schema Module> <Schema Table Name> [<Column Name>:<Column Type>]+
$ mix torch.gen.html Blog Post posts title:string body:text published_at:datetime published:boolean views:integer

The output would look like:

* creating priv/templates/phx.gen.html/edit.html.heex
* creating priv/templates/phx.gen.html/form.html.heex
...<omitted for brevity>...
* injecting test/phx1_6/blog_test.exs
* injecting test/support/fixtures/blog_fixtures.ex

Add the resource to your browser scope in lib/phx1_6_web/router.ex:

    resources "/posts", PostController


Remember to update your repository by running migrations:

    $ mix ecto.migrate

Ensure the following is added to your endpoint.ex:

    plug(
      Plug.Static,
      at: "/torch",
      from: {:torch, "priv/static"},
      gzip: true,
      cache_control_for_etags: "public, max-age=86400",
      headers: [{"access-control-allow-origin", "*"}]
    )

Also don't forget to add a link to layouts/torch.html if desired.

    <nav class="torch-nav">
      <!-- nav links here -->
    </nav>

Torch also installed an admin layout into your my_app_web/templates/layout/torch.html.heex. You will want to update it to include your new navigation link:

<nav class="torch-nav">
  <a href="/posts">Posts</a>
</nav>

There may be times when you are adding Torch into an already existing system where your application already contains the modules and controllers and you just want to use the Torch admin interface. Since the torch.gen mix tasks are just wrappers around the existing phx.gen tasks, you can use most of the same flags. To add an admin interface for Posts in the previous example, where the model and controller modules already exist, use the following command:

$ mix torch.gen.html Blog Post posts --no-schema --no-context --web Admin title:string body:text published_at:datetime published:boolean views:integer

Torch.Pagination customization

The following assumes you the above example when running torch.gen.html.

By default, the Torch generators added the following code to your Blog context module:

# blog.ex

  use Torch.Pagination,
    repo: MyApp.Repo,
    model: MyApp.Blog.Post,
    name: :posts

Please refer to the Torch.Pagination module for documentation on how to customize the pagination options for each model, or globally for your whole application.

NOTE If you want to customize the pagination functions themselves for your application, do not use the default Torch.Pagination as described above; instead you will need to define your own paginate_*/2 method that will return a Scrivener.Page object. You can also define your own pagination system and functions as well, but that will require further customization of the generated Torch controllers as well.

Association filters

Torch does not support association filters at this time. Filtrex does not yet support them.

You can checkout these two issues to see the latest updates:

rcdilorenzo/filtrex#55

rcdilorenzo/filtrex#38

However, that does not mean you can't roll your own.

Example

We have a Accounts.User model that has_many :credentials, Accounts.Credential and we want to support filtering users by credentials.email.

  1. Update the Accounts domain.
# accounts.ex
...
defp do_paginate_users(filter, params) do
  credential_params = Map.get(params, "credentials")
  params = Map.drop(params, ["credentials"])

  User
  |> Filtrex.query(filter)
  |> credential_filters(credential_params)
  |> order_by(^sort(params))
  |> paginate(Repo, params, @pagination)
end

defp credential_filters(query, nil), do: query

defp credential_filters(query, params) do
  search_string = "%#{params["email"]}%"

  from(u in query,
    join: c in assoc(u, :credentials),
    where: like(c.email, ^search_string),
    group_by: u.id
  )
end
...
  1. Update form filters.
# users/index.html.heex
<div class="field">
  <label>Credential email</label>
  <%= text_input(:credentials, :email, value: maybe(@conn.params, ["credentials", "email"])) %>
</div>

Note: You'll need to install & import Maybe into your views {:maybe, "~> 1.0.0"} for the above heex to work.

Styling

Torch generates two CSS themes you can use: base.css & theme.css. The base styles are basically bare bones, and the theme styles look like the screenshot above. Just change the stylesheet link in the torch.html.heex layout.

If you want to use the theme, but override the colors, you'll need to include your own stylesheet with the specific overrides.

Internationalization

Torch comes with .po files for several locales. If you are using Torch and can provide us with translation files for other languages, please submit a Pull Request with the translation file. We'd love to add as many translations as possible.

If you wish to add your own customized translations, you can configure Torch to use your own custom MessagesBackend and adding it in your Torch configuration settings in config.exs. You can find the all messages that can be customized in the default i18n/backend.ex file.

If you are customizing a backend for a "standard" spoken language, please submit back a proper .po translation file for us to include in the official Torch releases so other users can take advantage.

Example

defmodule MyApp.CustomMessagesBackend do
  def message("Contains"), do: "** CUSTOM Contains **"
  def message("Equals"), do: "** CUSTOM Equals ****"
  def message("< Prev"), do: "<--"
  def message("Next >"), do: "-->"

  # You can add a fallback so it won't break with newly added messages or
  # messages you did not customize
  def message(text), do: Torch.I18n.Backend.message(text)
end
# config.exs
config :torch,
  otp_app: :my_app_name,
  i18n_backend: MyApp.CustomMessagesBackend

Development

Getting Started

Torch currently uses Node 18 to build its assets.

Building the Torch asset bundles

The JavaScript bundle is output to priv/static/torch.js, and the CSS bundles are output to priv/static/base.css and priv/static/theme.css.

To build the bundles navigate to the assets folder and run the following commands:

$ cd assets
$ npm i
$ npm run compile

torch's People

Contributors

a-solovev avatar adamu avatar arcanemachine avatar chewnoill avatar codeithuman avatar cpjolicoeur avatar danielberkompas avatar darinwilson avatar debetux avatar dependabot-preview[bot] avatar dependabot[bot] avatar gantman avatar hanskenis avatar jamonholmgren avatar kianmeng avatar kimihito avatar krainboltgreene avatar lboekhorst avatar maennchen avatar matsu911 avatar mitchellhenke avatar morgandonze avatar nhphuc412 avatar noahduncan avatar optikfluffel avatar rvrx avatar scorsi avatar silasjmatson avatar yulolimum avatar zberkom avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

torch's Issues

Improve test coverage

Currently, we have almost no tests. Ideally, we'd generate into an example app and then run tests in the example app to verify that a) the functionality works and b) the generated tests work.

  • Move example app to the tests/support/apps directory
  • Add the usual bin scripts
  • Add integration test that generates torch context into the test/support/apps/example app, then run tests in that app
  • Install coveralls
  • Unit test all of the modules which make sense
  • Switch to Travis instead of Semaphore

Add .npmignore file

When including torch in the package.json with using torch: "file:deps/torch", it is including the entire dependency, when we only need a couple files:

image

Date Range Filter: match error if start or end date isn't selected

While using the date range filter, if you select just one date and leave the other one blank, you get a match error:

no match of right hand side value: {:error, "Invalid date value format: Both a start and end key are required."}

I think the JS should set the end date to current day if start date is less than today and if only start date was selected; set the start date to beginning of time if only the end date is selected.

Example app is currently broken -- Readme contains no instructions for compilation or tests

Since we switched to using webpack for asset compilation, the example app no longer works.

I think we should have a script for both brunch compilation and webpack compilation in the package.json.

Tasks:

  • Update & Fix Example App (w/2 ways to compile -- brunch & webpack)

Questions:

  • Should the example app reference the compiled assets in the root priv/static/ directory? I think it should reference them identically to how an app in the wild would do so. Not sure what this would entail.
  • Should the example app reference phoenix_html and phoenix from the root directory in the package.json? (It currently does: See here)
  • Add development/contribution guides
  • Add bin/ci script that builds and tests the project (using the example app)
  • Update CI server to fix issues (it's still passing even though it has issues, and it uses the bin/setup script, which is outdated)

From https://semaphoreci.com/ir/torch/branches/bug-64-javascript-error/builds/1

# lines removed
06:38:26.283 [error] GenServer #PID<0.4513.0> terminating
** (KeyError) key :password not found in: [types: Postgrex.DefaultTypes, username: "runner", backoff_type: :stop, pool: DBConnection.Connection, database: "template1", otp_app: :example, repo: Example.Repo, adapter: Ecto.Adapters.Postgres, hostname: "localhost", pool_size: 10]
    (elixir) lib/keyword.ex:333: Keyword.fetch!/2
    (postgrex) lib/postgrex/protocol.ex:581: Postgrex.Protocol.auth_md5/4
    (postgrex) lib/postgrex/protocol.ex:475: Postgrex.Protocol.handshake/2
    (db_connection) lib/db_connection/connection.ex:134: DBConnection.Connection.connect/2
    (connection) lib/connection.ex:622: Connection.enter_connect/5
    (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: nil
State: Postgrex.Protocol
** (Mix) The database for Example.Repo couldn't be created: an exception was raised:
    ** (KeyError) key :password not found in: [types: Postgrex.DefaultTypes, username: "runner", backoff_type: :stop, pool: DBConnection.Connection, database: "template1", otp_app: :example, repo: Example.Repo, adapter: Ecto.Adapters.Postgres, hostname: "localhost", pool_size: 10]
        (elixir) lib/keyword.ex:333: Keyword.fetch!/2
        (postgrex) lib/postgrex/protocol.ex:581: Postgrex.Protocol.auth_md5/4
        (postgrex) lib/postgrex/protocol.ex:475: Postgrex.Protocol.handshake/2
        (db_connection) lib/db_connection/connection.ex:134: DBConnection.Connection.connect/2
        (connection) lib/connection.ex:622: Connection.enter_connect/5
        (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
npm ERR! addLocal Could not install /home/runner/deps/phoenix
npm ERR! addLocal Could not install /home/runner/deps/phoenix_html
-npm ERR! Linux 3.13.0-61-generic
npm ERR! argv "/home/runner/.nvm/versions/node/v4.6.2/bin/node" "/home/runner/.nvm/versions/node/v4.6.2/bin/npm" "install"
npm ERR! node v4.6.2
npm ERR! npm  v2.15.11
npm ERR! path /home/runner/deps/phoenix
npm ERR! code ENOENT
npm ERR! errno -2
npm ERR! syscall open

npm ERR! enoent ENOENT: no such file or directory, open '/home/runner/deps/phoenix'
npm ERR! enoent This is most likely not a problem with npm itself
npm ERR! enoent and is related to npm not being able to find a file.
npm ERR! enoent 

npm ERR! Please include the following file with any support request:
npm ERR!     /home/runner/torch/example/npm-debug.log

# lines removed

warning: the dependency :postgrex requires Elixir "~> 1.3.4 or ~> 1.4" but you are running on v1.3.0

# lines removed

Support nested models

You currently have to jump through a few extra hoops if you want to have torch forms for e.g. a model on the other side of a has_many relationship. I'm not sure how practical this would be, but I wonder if it would be possible to do something like:

mix torch.gen eex Admin Post:Comment comments body:text inserted_at:date

and have it do the right thing(s).

Properly disable form inputs

Recent changes to torch.js broke how it disables form inputs. I fixed it manually on one of our projects, and here's the updated code that works:

import Pikaday from 'pikaday'

window.onload = () => {
  const slice = Array.prototype.slice
  /*
   * Prevent empty fields from being submitted, since this breaks Filtrex.
   */
  const formFilters = document.querySelector('form#filters')
  if (!formFilters) return

  formFilters.addEventListener('submit', function (e) {
    e.preventDefault()

    let canSubmit = true

    slice.call(this.querySelectorAll('.field'), 0).forEach((field) => {
      let text = field.getElementsByTagName('label')[0].textContent
      let start = field.getElementsByClassName('start')[0]
      let end = field.getElementsByClassName('end')[0]

      if (start && end) {
        if (start.value === '' && end.value !== '') {
          window.alert(`Please select a start date for the ${text} field`)
          canSubmit = false
        } else if (end.value === '' && start.value !== '') {
          window.alert(`Please select a end at date for the ${text} field`)
          canSubmit = false
        }
      }
    })

    if (canSubmit) {
      slice.call(this.querySelectorAll('input, select'), 0).forEach((field) => {
        if (field.value === '') {
          field.disabled = true
        }
      })

      e.target.submit()
    }
  })

  slice.call(document.querySelectorAll('select.filter-type'), 0).forEach((field) => {
    field.addEventListener('change', (e) => {
      e.target.nextElementSibling.name = e.target.value
    })
  })

  slice.call(document.querySelectorAll('.datepicker'), 0).forEach((field) => {
    new Pikaday({field: field})
  })
}

Move example app into separate repo or subdirectory

I'd like to propose switching torch from being structured as an umbrella app, to either having the example app in a separate repo, or pushing it down into a separate subdirectory (like we're doing with thesis-phoenix.

The biggest bummer about the current arrangement is that it's impossible to set up the torch dependency to point to your own git branch because the dependency format in mix.exs does not support pointing to a subdirectory (AFAIK). This means you can only use the current official hex package, or a repo on your local filesystem. Pointing your deps at github branches is not ideal, but it's a necessary evil, particularly in these early stages.

I'm open to other possibilities, but the current setup is a little clumsy when developing new features.

Edeliver and distillery deployment not working

I am trying to deploy my elixir app with edeliver and distellly but I am having the issue

no such file or directory '.../builds/deps/torch/node_modules/bourbon'

I am using:

  • Elixir 1.6.5 (compiled with OTP 19)
  • Node v10.1.0
  • Phoenix 1.2.4
  • erlang 18.3.4.8

And using the standard edeliver configuration with:

npm install
./node_modules/brunch/bin/brunch build --production

Do you have an idea of what I am missing here?

Thanks

Normalize CSS file should only affect torch markup

Here's the issue.

@import my_app_css_files.sass
@import torch_css_files.sass

The app css files are included before torch so you can style torch using my_app variables / mixins / etc. Torch include a css reseter that isn't namespaced.

Two options:

  • namespace normalize
    or...
  • include snippet in docs to make sure people load torch before my_app css files.

Here's an example of how you can namespace the normalize file.

Add an ID to the body in the admin.html layout.

Use a mixin with the normalize styles: https://github.com/infinitered/thesis-phoenix/blob/master/web/static/css/_mixins/_normalize.scss (or create your own, stripping html and body styles).

Include the mixin on the body ID you created: https://github.com/infinitered/thesis-phoenix/blob/master/web/static/css/base/_global.sass#L9


** Thesis files are for reference

Filter select view function should support custom options

The filter_select method should support a 4th optional agument that overrides the default options of "Contains" & "Equals".

Current state:
= filter_select(:agency, :approved, @conn.params)

Optional override:
= filter_select(:agency, :approved, ["Yes": true, "No": false], @conn.params)

last release Phoenix 1.3... throws error

After following the instructions...I ran mix reps.get & I got:

Running dependency resolution...

Failed to use "phoenix" (version 1.3.0-rc.3) because
  apps/insc_web/mix.exs requires ~> 1.3.0-rc
  phoenix_live_reload (version 1.0.8) requires ~> 1.0 or ~> 1.2-rc
  torch (version 1.0.0-rc.5) requires ~> 1.2 *

* This requirement does not match pre-releases. To match pre-releases include a pre-release in the requirement, such as: "~> 2.0-beta".

** (Mix) Hex dependency resolution failed, relax the version requirements of your dependencies or unlock them (by using mix deps.update or mix deps.unlock). If you are unable to resolve the conflicts you can try overriding with {:dependency, "~> 1.0", override: true}

mix torch.install - Phoenix.HTML.Form.date_input/3 conflicts with local function

== Compilation error in file lib/torch/views/filter_view.ex ==
** (CompileError) lib/torch/views/filter_view.ex:176: imported Phoenix.HTML.Form.date_input/3 conflicts with local function
    (elixir) src/elixir_locals.erl:89: :elixir_locals."-ensure_no_import_conflict/3-lc$^0/1-0-"/2
    (elixir) src/elixir_locals.erl:89: anonymous fn/3 in :elixir_locals.ensure_no_import_conflict/3
    (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
    (elixir) lib/kernel/parallel_compiler.ex:198: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/6
could not compile dependency :torch, "mix compile" failed. You can recompile this dependency with "mix deps.compile torch", update it with "mix deps.update torch" or clean it with "mix deps.clean torch"

UndefinedFunctionError "module Routes is not available"

Hey there and thanks for torch.
So I'm setting up the library, and every route helper function like "admin_model_path" is preceded by the namespace "Routes". That throws the error shown in this issue title.
I think it works fine if I delete every instance of "Routes" before every function, but maybe i'm missing something? Nonetheless it's a bit of a pain to have to manually delete those

Add a "Starts With" filter option

Something that is a nice to have but not necessarily useful to everybody - a "starts with" filter.

A lot of times you see this filter as a list of letters from A to Z that you can click on (http://d.pr/i/qP7r); but, it could instead be added to the dropdown.

Thoughts?

No torch.html generated when used with Phoenix 1.4

Thanks for the great packages, looks very promising.

I went through the readme with phoenix 1.4.

It looks like the torch.html is not generated in the layout folder.

Is the torch.html somewhere in your repo such that I can add it manually?

Clicking pagination arrows resets sort.

If you click on a table heading cell to sort by column, you can navigation throughout the paginated pages without issues IF you click on the numbers. As soon as you click on next or prev arrows, the sort resets to the default (id, asc).

Clicking on pagination link reset filters

I use the filters to create a query. Here's the URL:

http://localhost:4000/internal/internal-news?_utf8=%E2%9C%93&filters%5B%5D=intranet_news%5Btitle_contains%5D&filters%5B%5D=intranet_news%5Bbody_contains%5D&intranet_news%5Bbody_contains%5D=we

After getting the results, I click to see the next page of results. The pagination link is as follows:

http://localhost:4000/internal/internal-news?page=2&sort_field=id&sort_direction=asc

This resets the filters.

On a side note, the pagination works great with the the query param sorting.

@darinwilson @danielberkompas

torch does not work with ecto >= 2.2.0

Since you're setting {:ecto, "~> 2.1.2"}, and since ecto 2.2 was released a few days ago, now torch does not play well with the updated ecto package.
I would assume it's only a matter of changing the requirement from ~> 2.1.2 to ~> 2.1 since minor releases should not introduce any backward incompatible changes.

Readme contains incorrect command

The readme under torch/apps/torch has:

mix torch.gen.html Admin Post posts title:string body:text inserted_at:date

should be:

mix torch.gen eex Admin Post posts title:string body:text inserted_at:date

@danielberkompas

Generate files into proper umbrella apps

The torch.install and torch.gen tasks should act relative to the Application.app_dir of the configured torch application.

The torch.gen tasks should throw errors when they are run at the top level of an umbrella application, like the phoenix.gen tasks do.

"** (Mix) mix phx.gen.html can only be run inside an application directory"

Torch can't find infinite-red-logo

Once again, after the css reorganization, the logo can't be found and is crashing the asset compile.

../images/infinite-red-logo.svg should be /images/infinite-re...

Filtrex is missing from install dependencies

After going through all the installation steps, running mix phoenix.server, I get the following error:

** (Mix) Could not start application filtrex: could not find application file: filtrex.app

I've tried cleaning deps, reinstalling, updating - nothing fixes. Added filtrex manually to my mix.exs, the app starts up fine; however, I see that you are pulling in a fork, so I'm not sure how to proceed.

@danielberkompas

test integration with coherence

Hello,

Torch test, have hard time working with coherence wouldn it be great to have some integration with it?
I m willing to do a pull request about that. Any thoughts?

Thanks

NPM package will not install

NPM 5.0.1
Node 7.10.0

The npm package that should get installed from the hex deps does not get installed. Right now, the work around for a couple apps is to bring in the js and sass files manually by copying into the project.

Issue with fonts.

From what I'm seeing, the css directory was recently restructured.
I'm getting this error:
http://d.pr/i/vdVV1f+

Since the the reference to fonts should probably be ../../../fonts/fontaw......

Filter by has_one association field

I have two models โ€“ User and Credential. My admin UI shows a list of users and has some filters on the sidebar. It's pretty easy to add a filter for a model field, but I didn't find any examples in the documentation explaining filtration mechanism for associations. Is it possible to filter user records by credential.email without hacking do_paginate_users/2 much?

@doc """
Paginate the list of users using filtrex
filters.

## Examples

    iex> list_users(%{})
    %{users: [%User{}], ...}
"""
@spec paginate_users(map) :: {:ok, map} | {:error, any}
def paginate_users(params \\ %{}) do
  params =
    params
    |> Map.put_new("sort_direction", "desc")
    |> Map.put_new("sort_field", "inserted_at")

  {:ok, sort_direction} = Map.fetch(params, "sort_direction")
  {:ok, sort_field} = Map.fetch(params, "sort_field")

  with {:ok, filter} <- Filtrex.parse_params(filter_config(:users), params["user"] || %{}),
       %Scrivener.Page{} = page <- do_paginate_users(filter, params) do
    {:ok,
     %{
       users: page.entries,
       page_number: page.page_number,
       page_size: page.page_size,
       total_pages: page.total_pages,
       total_entries: page.total_entries,
       distance: @pagination_distance,
       sort_field: sort_field,
       sort_direction: sort_direction
     }}
  else
    {:error, error} -> {:error, error}
    error -> {:error, error}
  end
end

defp do_paginate_users(filter, params) do
  User
  |> Filtrex.query(filter)
  |> order_by(^sort(params))
  |> preload(:credential)
  |> paginate(Repo, params, @pagination)
end

defp filter_config(:users) do
  defconfig do
    text :username
    date :inserted_at
  end
end

Maybe I need to extend filter_config/1 and/or Filtrex.parse_params(filter_config(:users), params["user"] || %{})?

I'm facing almost the same problem when trying to order records.

Support Phoenix 1.3

We need to update Torch to support Phoenix 1.3 in an idiomatic way.

Generators

Our generator is too complicated and hard to use. It would be better if we went to a CLI approach.

Here's how you would build a new Torch area from scratch:

# First, generate the Schema using the Phoenix generator
$ mix phx.gen.schema MyApp.Post ...

$ mix torch.gen

What schema module do you want to use?

> MyApp.Post

What do you want to use as the key?
Options: [:id, :uuid]

> id

Which fields do you want to be able to filter on?
Options: [:title, :author, :body, ...]

> title,author,body,published

What context module do you want to generate business logic into?

> MyApp.Writing

What is the name of the `repo` module?

> MyApp.Repo

What should the controller be named?

> MyAppWeb.PostController

Does a user need to have specific permissions to edit `MyApp.Post`s?
yes/no

> yes

What is the name of the user module?

> MyApp.User

Generating...
Done!

Context

This is the code that we should append to the bottom of the given context.

alias MyApp.Repo
alias MyApp.Post
alias MyApp.User, warn: false

@post_filters [
  %Filtrex.Type.Config{type: :text, keys: ~w(title author)}
]

@doc """
Paginates posts.

## Example

    Writing.paginate_posts(%{
      "post" => %{
        "title_equals" => "Hello, World!"
      },
      "page" => 2
    }, %User{...})
"""
@spec paginate_posts(map, User.t) ::
  {:ok, Scrivener.Page.t} |
  {:error, String.t}
def paginate_posts(params, _current_user) do
  with {:ok, filter} <- Filtrex.parse(params["post"], @post_filters) do
    page =
      Post
      |> Filtrex.query(filter)
      |> Repo.paginate(params)

    {:ok, page}
  end
end

@doc """
Gets a post by ID.

## Example

    Writing.get_post(123, %User{...})
"""
@spec get_post(integer, User.t) :: 
  {:ok, post} |
  {:error, :not_found}
def get_post(id, _current_user) do
  post = Repo.get_by(Post, id)
  if post, do: {:ok, post}, else: {:error, :not_found}
end

@doc """
Returns an `Ecto.Changeset` for a post.

## Examples

    Writing.change_post(%Post{}, %User{})
    # => {:ok, %Ecto.Changeset{data: %Post{}, ...}}

    Writing.change_post(%Post{}, %{"title" => "Hello, World!"}, %User{})
    # => {:ok, %Ecto.Changeset{data: %Post{}, ...}}
"""
@spec change_post(Post.t, map, User.t) ::
  {:ok, Ecto.Changeset.t}
def change_post(post, params \\ %{}, _current_user) do
  {:ok, Post.changeset(post, params)}
end

@doc """
Creates a post.

## Examples

    Writing.create_post(%{"title" => "Hello, World!"}, %User{})
    # => {:ok, %Post{title: "Hello, World!"}}

    Writing.create_post(%{"title" => ""}, %User{})
    # => {:error, %Ecto.Changeset{...}}
"""
@spec create_post(map, User.t) ::
  {:ok, Post.t} |
  {:error, Ecto.Changeset.t}
def create_post(params, _current_user) do
  with {:ok, changeset} <- change_post(%Post{}, params) do
    Repo.insert(changeset)
  end
end

@doc """
Updates a post.

## Examples

    Writing.update_post(post, %{"title" => "Best Post Ever"}, %User{})
    # => {:ok, %Post{title: "Best Post Ever"}}

    Writing.update_post(post, %{"title" => ""}, %User{})
    # => {:error, %Ecto.Changeset{...}}
"""
@spec update_post(Post.t, params, User.t) ::
  {:ok, Post.t} |
  {:error, Ecto.Changeset.t}
def update_post(post, params, _current_user) do
  with {:ok, changeset} <- change_post(post, params) do
    Repo.update(changeset)
  end
end

@doc """
Deletes a post.

## Examples

    Writing.delete_post(post, %User{})
    # => {:ok, %Post{...}}

    Writing.delete_post(post, %User{})
    # => {:error, %Ecto.Changeset{...}}
"""
@spec delete_post(Post.t) ::
  {:ok, Post.t} |
  {:error, Ecto.Changeset.t}
def delete_post(post, _current_user) do
  post
  |> change_post
  |> Repo.delete
end

Controller

defmodule MyAppWeb.PostController do
  @moduledoc """
  Torch-generated controller to manage Posts.
  """

  use MyAppWeb, :controller
  use Torch.Controller, assigns: [:current_user]

  alias MyApp.Writing
  alias MyAppWeb.TorchFallbackController

  plug :assign_post when action in [:edit, :update, :delete]

  action_fallback TorchFallbackController

  def index(conn, params, current_user) do
    with {:ok, page} <- Writing.paginate_posts(params, current_user) do
      conn
      |> assign_pagination(page)
      |> render("index.html")
    end
  end

  def new(conn, _params, current_user) do
    with {:ok, changeset} <- Writing.change_post(%Post{}, current_user) do
      render(conn, "new.html", changeset: changeset)
    end
  end

  def create(conn, params, current_user) do
    with {:ok, post} <- Writing.create_post(params["post"], current_user) do
      conn
      |> put_flash(:success, "Post created!")
      |> redirect(to: post_path(conn, :index))
    end
  end

  def edit(conn, _params, current_user) do
    with {:ok, changeset} <- Writing.change_post(conn.assigns.post, current_user) do
      render(conn, "edit.html", changeset: changeset)
    end
  end

  def update(conn, params, current_user) do
    with {:ok, post} <- Writing.update_post(conn.assigns.post, params["post"], current_user) do
      conn
      |> put_flash(:success, "Post updated!")
      |> redirect(to: post_path(conn, :index))
    end
  end

  def delete(conn, _params, current_user) do
    with {:ok, _post} <- Writing.delete_post(conn.assigns.post, current_user) do
      conn
      |> put_flash(:success, "Post deleted!")
      |> redirect(to: post_path(conn, :index))
    end
  end

  defp assign_post(conn, _opts) do
    case Writing.get_post(conn.params["id"]) do
      {:ok, post} ->
        assign(conn, :post, post)
      error ->
        conn
        |> TorchFallbackController.call(error)
        |> halt
    end
  end

  defp assign_fallback_opts(conn, _opts) do
    assign(conn, :fallback, %{
      index_path: post_path(conn, :index)
    })
  end
end

Fallback Controller

This helps us DRY up our controller code.

defmodule MyAppWeb.TorchFallbackController do
  import Plug.Conn

  def call(conn, {:error, :not_found}) do
    conn
    |> put_flash(:error, "Record not found!")
    |> redirect(to: conn.assigns.fallback.index_path)
  end

  def call(conn, {:error, <<"Unknown filter key", _rest::binary>> = error}) do
    conn
    |> put_flash(:error, "Invalid search: #{inspect(error)}")
    |> redirect(to: conn.assigns.index_path)
  end

  def call(conn, {:error, %Ecto.Changeset{data: %{id: id}} = changeset})
  when id == nil do
    conn
    |> validation_error(changeset)
    |> render("new.html")
  end

  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> validation_error(changeset)
    |> render("edit.html")
  end

  defp validation_error(conn, changeset) do
    conn
    |> put_flash(:error, "Oops! There were errors on the form!")
    |> assign(:changeset, changeset)
  end
end

View

defmodule MyAppWeb.PostView do
  use MyAppWeb, :view

  import Torch.View
end

Templates

Pretty much unchanged from what we have now.

Update path's and configs to work with phoenix 1.3

The actual project do not use phx 1.3 context, so the install(mix torch.install exx) are using phx 1.2 path's.

Also the config in brunch-config should have something like that to load the css:

  stylesheets: {
      joinTo: {
        'css/app.css': /^(web|node_modules|css)/
      }
    }

Add ability to set default sort order

Right now, the default sort order is by id, ascending.

I've tried adding default sort order by appending query strings in the admin.html:
to: internal_intranet_news_path(@conn, :index) <> "?sort_field=inserted_at&sort_direction=desc"

Apart from breaking the "active" state of the nav link, this works great. This also works with paginated pages.

Since the active link is important, I tried to set default order by setting it in the controller:

    params = Map.put_new(params, "sort_field", "inserted_at")
    params = Map.put_new(params, "sort_direction", "desc")

This is probably cleaner, but this breaks pagination. When clicking on a pagination link, it adds the default query strings (id and asc).

Ideas?

Example app post index controller error

I got the following error trying out the example app:

[debug] Processing by Example.Admin.PostController.index/2
  Parameters: %{}
  Pipelines: [:browser]
[debug] QUERY OK source="authors" db=0.9ms queue=2.8ms
SELECT a0."id", a0."name", a0."email", a0."inserted_at", a0."updated_at" FROM "authors" AS a0 []
[%Example.Author{__meta__: #Ecto.Schema.Metadata<:loaded, "authors">,
  email: "blee", id: 5, inserted_at: #Ecto.DateTime<2017-02-27 16:46:51>,
  name: "blee", updated_at: #Ecto.DateTime<2017-02-27 16:46:51>}]
[info] Sent 500 in 16ms
[error] #PID<0.541.0> running Example.Endpoint terminated
Server: localhost:4000 (http)
Request: GET /admin/posts
** (exit) an exception was raised:
    ** (KeyError) key :author not found in: %Example.Author{__meta__: #Ecto.Schema.Metadata<:loaded, "authors">, email: "blee", id: 5, inserted_at: #Ecto.DateTime<2017-02-27 16:46:51>, name: "blee", updated_at: #Ecto.DateTime<2017-02-27 16:46:51>}
        (example) web/controllers/admin/post_controller.ex:103: anonymous fn/1 in Example.Admin.PostController.assign_authors/2
        (elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
        (example) web/controllers/admin/post_controller.ex:103: Example.Admin.PostController.assign_authors/2

It only happens when there is at least one author or category. If there are no author or category records the error does not occur.

The problem in the example code occurs here: torch/apps/example/web/controllers/admin/post_controller.ex

  defp assign_authors(conn, _opts) do
    authors =
      Example.Author
      |> Repo.all
     |> Enum.map(&({&1.author, &1.id}))
    assign(conn, :authors, authors)
end

|> Enum.map(&({&1.author, &1.id}))

"author" is the name of the table not the field "name" which gets loaded into conn for the filter. Changing &1.author to &1.name and doing the same for assign_categories makes the Post index work when an Author or Category has been defined.

The codegen assoc_plug_definitions(binding, attrs) in torch/lib/mix/tasks/torch.gen.ex uses :display_name. I've not looked any deeper, I'm a bit out of my depth at this point.

Thanks for making torch btw ๐Ÿ‘

License?

It looks like this project does not currently have a license associated with it - could you add one, to make it possible for folks to use it?

References:

I do see that many of this organization's other projects are licensed under MIT, so maybe that makes sense? On the other hand, you have existing contributors, and I'm not sure of the ramifications involving them.

`page` query string is blank when using the sorting

Clicking on a table heading appends query strings to the URL in order to sort the data items. It should add a sort_field and sort_direction key to the params (which it does). However, it also adds a page key which is blank.

Here are relevant code snippets:

We are passing in opts with no page key here:

We are setting the page key here - in this case it's nil because i dont have page in the assigns:

This results in the following error:

Am I missing something? Do I have to set the page key on the conn in the controller?

Hex version not updated, 2.0.0-alpha fails.

The following fails, as the package is not updated yet on Hex:

{:torch, "~> 2.0.0-alpha"}

** (Mix) No matching version for torch ~> 2.0.0-alpha (from: mix.exs) in registry

I had to specify github, I believe the tag is optional:

{:torch, github: "infinitered/torch", tag: "v2.0.0-rc.1"},

Pagination `end_page/3` is broken

I found this bug when looking at Torch's pagination_view.ex for inspiration on another project.

Consider the following inputs for the end_page/3 function (found here):

current_page: 1
total (or total_pages): 5
distance: 5

The current_page is less than the distance which causes the function to return distance * 2 which is 10, but 10 is greater than total/total_pages. This, in turn, creates addition links to pages that do not exist.

I will be submitting a PR that fixes this issue shortly.

Add support for read-only fields

There are cases where fields should be visible to admins, but not editable, e.g. timestamps. The user could specify this on the command line by appending :readonly when generating the admin area on the command line:

$ mix torch.gen eex Admin Post posts title:string body:text inserted_at:date:readonly

The generated template could either put the value in a disabled input, or just display it as plain text. The controller would exclude read-only fields when creating changesets.

Improve documentation

Ensure that we have full documentation and guides on how to use everything in Torch, including with umbrella apps.

Javascript Errors in Safari

Attempting to use the filters causes an error in Safari, due to NodeList not having a defined `forEach function.

TypeError: this.querySelectorAll('input, select').forEach is not a function. (In 'this.querySelectorAll('input, select').forEach(function (field) {
      if (field.value === '') {
        field.disabled = true;
      }
    })', 'this.querySelectorAll('input, select').forEach' is undefined)

Brunch not building correct JS imports

Brunch is not building correct JS imports for use in an NPM Package. You can see my current project on the moment-js-error. The require.register("assets/js/torch.js",function()...) found in priv/static/torch.js causes a console error when the Torch NPM package is used in another project. Therefore, Pikaday fields do not work.

At this point, after research and trying various solutions, I'd recommend replacing Brunch with another asset building like Webpack.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.