form improvements

pull/26/head
Ondřej Hruška 6 years ago
parent 3242ae9cbe
commit a9ffd378a0
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 3
      app/Http/Controllers/Auth/RegisterController.php
  2. 24
      app/Http/Controllers/TableController.php
  3. 3
      app/Models/User.php
  4. 14
      app/View/CheckboxWidget.php
  5. 36
      app/View/Widget.php
  6. 33
      app/View/WidgetFactory.php
  7. 3
      database/migrations/2014_10_12_000000_create_users_table.php
  8. 1
      database/migrations/2018_07_08_193700_create_tables_table.php
  9. 2
      resources/assets/sass/_bst-modules.scss
  10. 5
      resources/assets/sass/app.scss
  11. 148
      resources/views/auth/login.blade.php
  12. 55
      resources/views/auth/register.blade.php
  13. 2
      resources/views/form/_help.blade.php
  14. 22
      resources/views/form/checkbox.blade.php
  15. 4
      resources/views/form/input.blade.php
  16. 4
      resources/views/form/textarea.blade.php
  17. 31
      resources/views/table/create.blade.php

@ -49,7 +49,7 @@ class RegisterController extends Controller
protected function validator(array $data) protected function validator(array $data)
{ {
return Validator::make($data, [ return Validator::make($data, [
'name' => 'required|string|max:255', 'name' => 'required|string|max:255|regex:/^[a-zA-Z0-9_.-]+$/',
'email' => 'required|string|email|max:255|unique:users', 'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed', 'password' => 'required|string|min:6|confirmed',
]); ]);
@ -65,6 +65,7 @@ class RegisterController extends Controller
{ {
return User::create([ return User::create([
'name' => $data['name'], 'name' => $data['name'],
'title' => $data['name'], // display name - by default, init to name
'email' => $data['email'], 'email' => $data['email'],
'password' => Hash::make($data['password']), 'password' => Hash::make($data['password']),
]); ]);

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Table;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class TableController extends Controller class TableController extends Controller
@ -13,7 +14,7 @@ class TableController extends Controller
*/ */
public function create() public function create()
{ {
$exampleColSpec = $exampleColumns =
"latin, string, Latin Name\n". "latin, string, Latin Name\n".
"common, string, Common Name\n". "common, string, Common Name\n".
"lifespan, int, Lifespan (years)"; "lifespan, int, Lifespan (years)";
@ -24,21 +25,32 @@ class TableController extends Controller
"Patella vulgata, common limpet, 20"; "Patella vulgata, common limpet, 20";
return view('table.create', return view('table.create',
compact('exampleColSpec', 'exampleData') compact('exampleColumns', 'exampleData')
); );
} }
public function storeNew(Request $request) public function storeNew(Request $request)
{ {
$u = \Auth::user();
$this->validate($request, [ $this->validate($request, [
'table-name' => 'required', 'name' => 'required',
'table-descr' => 'string|nullable', 'title' => 'string|nullable',
'description' => 'string|nullable',
'license' => 'string|nullable', 'license' => 'string|nullable',
'upstream' => 'string|nullable', 'upstream' => 'string|nullable',
'col-spec' => 'required', 'columns' => 'required',
'row-data' => 'string|nullable', 'data' => 'string|nullable',
]); ]);
// Check if table name is unique for user
$name = $request->get('name');
if ($u->tables()->where('name', $name)->exists()) {
return back()->withErrors([
'title' => "A table called \"$name\" already exists in your account.",
]);
}
return "Ok."; return "Ok.";
} }
} }

@ -16,6 +16,7 @@ use Illuminate\Notifications\Notification;
* @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at * @property \Carbon\Carbon $updated_at
* @property string $name - unique, for vanity URL * @property string $name - unique, for vanity URL
* @property string $title - for display
* @property string $email - unique, for login and social auth chaining * @property string $email - unique, for login and social auth chaining
* @property string $password - hashed pw * @property string $password - hashed pw
* @property Proposal[]|Collection $proposals * @property Proposal[]|Collection $proposals
@ -40,7 +41,7 @@ class User extends Authenticatable
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'name', 'email', 'password', 'name', 'title', 'email', 'password',
]; ];
/** /**

@ -0,0 +1,14 @@
<?php
namespace App\View;
class CheckboxWidget extends Widget
{
public function checked($condition=true)
{
$this->value = (bool)$condition;
return $this;
}
}

@ -12,14 +12,17 @@ namespace App\View;
* @property-read string|null $help * @property-read string|null $help
* @property-read string|null $value * @property-read string|null $value
* @property-read string $attributes * @property-read string $attributes
* @property-read int $labelCols
* @property-read int $fieldCols
*/ */
class Widget class Widget
{ {
private $attributes = [], protected $attributesArray = [],
$id, $name, $label, $value, $id, $name, $label, $value,
$viewName, $viewName,
$help, $help,
$style = []; $labelCols, $fieldCols,
$styleArray = [];
public function __construct($viewName, $name, $label) public function __construct($viewName, $name, $label)
{ {
@ -36,7 +39,7 @@ class Widget
'height', 'minHeight', 'maxHeight' 'height', 'minHeight', 'maxHeight'
]; ];
if (empty($args)) $args = ['']; if (empty($args)) $args = [null];
$arg = $args[0]; $arg = $args[0];
@ -50,7 +53,7 @@ class Widget
$this->$method = $arg; $this->$method = $arg;
} }
else { else {
$this->attributes[$method] = $arg; $this->attributesArray[$method] = $arg;
} }
return $this; return $this;
@ -58,7 +61,15 @@ class Widget
public function css($prop, $val) public function css($prop, $val)
{ {
$this->style[$prop] = $val; $this->styleArray[$prop] = $val;
return $this;
}
/** Apply a given layout (bootstrap grid, 12 cols total) */
public function layout($labelCols, $fieldCols)
{
$this->labelCols = $labelCols;
$this->fieldCols = $fieldCols;
return $this; return $this;
} }
@ -82,14 +93,19 @@ class Widget
public function compileAttribs() public function compileAttribs()
{ {
// compile attribs string // compile attribs string
$attribs_s = array_reduce(array_keys($this->attributes), function ($carry, $key) { $attribs_s = array_reduce(array_keys($this->attributesArray), function ($carry, $key) {
return $carry . ' ' . $key . '="' . e($this->attributes[$key]) . '"'; if ($this->attributesArray[$key] === null) {
// simple attribs like 'required'
return $carry . ' ' . $key;
} else {
return $carry . ' ' . $key . '="' . e($this->attributesArray[$key]) . '"';
}
}, ''); }, '');
// add a compiled list of styles // add a compiled list of styles
if (!empty($this->style)) { if (!empty($this->styleArray)) {
$attribs_s .= 'style="'.trim(e(array_reduce(array_keys($this->style), function ($carry, $key) { $attribs_s .= 'style="'.trim(e(array_reduce(array_keys($this->styleArray), function ($carry, $key) {
return $carry . $key . ': ' . $this->style[$key] . '; '; return $carry . $key . ': ' . $this->styleArray[$key] . '; ';
}, ''))).'"'; }, ''))).'"';
} }

@ -5,23 +5,48 @@ namespace App\View;
class WidgetFactory class WidgetFactory
{ {
private $labelCols = 3, $fieldCols = 8;
/** Configure layout used for future elements */
public function setLayout($labelCols, $fieldCols)
{
$this->labelCols = $labelCols;
$this->fieldCols = $fieldCols;
}
private function baseWidget($view, $name, $label)
{
return (new Widget($view, $name, $label))->layout($this->labelCols, $this->fieldCols);
}
public function text($name, $label) public function text($name, $label)
{ {
return new Widget('input', $name, $label); return $this->baseWidget('input', $name, $label);
}
public function password($name, $label)
{
return $this->baseWidget('input', $name, $label)->type('password');
} }
public function number($name, $label) public function number($name, $label)
{ {
return new Widget('number', $name, $label); return $this->baseWidget('input', $name, $label)->type('number');
} }
public function email($name, $label) public function email($name, $label)
{ {
return new Widget('email', $name, $label); return $this->baseWidget('input', $name, $label)->type('email');
}
public function checkbox($name, $label)
{
return (new CheckboxWidget('checkbox', $name, $label))
->layout($this->labelCols, $this->fieldCols);
} }
public function textarea($name, $label) public function textarea($name, $label)
{ {
return (new Widget('textarea', $name, $label))->minHeight('4em'); return $this->baseWidget('textarea', $name, $label)->minHeight('4em');
} }
} }

@ -17,7 +17,8 @@ class CreateUsersTable extends Migration
$table->increments('id'); $table->increments('id');
$table->timestamps(); $table->timestamps();
$table->string('name')->unique(); $table->string('name')->unique();
$table->string('email')->unique()->nullable(); $table->string('title')->index();
$table->string('email')->unique(); // would have to be nullable if we supported login via providers that don't give us e-mail
$table->string('password')->nullable(); $table->string('password')->nullable();
$table->rememberToken(); $table->rememberToken();
}); });

@ -19,6 +19,7 @@ class CreateTablesTable extends Migration
$table->unsignedInteger('owner_id'); $table->unsignedInteger('owner_id');
$table->unsignedInteger('ancestor_id')->index()->nullable(); $table->unsignedInteger('ancestor_id')->index()->nullable();
$table->unsignedInteger('revision_id')->index(); // active revision $table->unsignedInteger('revision_id')->index(); // active revision
$table->string('name')->index(); // indexable
$table->string('title')->index(); // indexable $table->string('title')->index(); // indexable
$table->text('description'); $table->text('description');
$table->text('license'); $table->text('license');

@ -11,7 +11,7 @@
@import "~bootstrap/scss/dropdown"; @import "~bootstrap/scss/dropdown";
@import "~bootstrap/scss/button-group"; @import "~bootstrap/scss/button-group";
//@import "~bootstrap/scss/input-group"; //@import "~bootstrap/scss/input-group";
//@import "~bootstrap/scss/custom-forms"; @import "~bootstrap/scss/custom-forms";
//@import "~bootstrap/scss/nav"; //@import "~bootstrap/scss/nav";
@import "~bootstrap/scss/navbar"; @import "~bootstrap/scss/navbar";
@import "~bootstrap/scss/card"; @import "~bootstrap/scss/card";

@ -11,6 +11,11 @@ html {
overflow-y: scroll; overflow-y: scroll;
} }
.form-help {
display: inline-block;
margin-top: $form-text-margin-top;
}
.tooltip-inner { .tooltip-inner {
text-align: left; text-align: left;
} }

@ -1,102 +1,62 @@
@extends('layouts.app') @extends('layouts.app')
@section('content') @section('content')
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-8"> <div class="col-md-8">
<div class="card"> <div class="card">
<div class="card-header">{{ __('Login') }}</div> <div class="card-header">{{ __('Login') }}</div>
<div class="card-body"> <div class="card-body">
<form method="POST" action="{{ route('login') }}" aria-label="{{ __('Login') }}"> <form method="POST" action="{{ route('login') }}" aria-label="{{ __('Login') }}">
@csrf @csrf
<div class="form-group row"> @php(Widget::setLayout(4, 6))
<label for="email" class="col-sm-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
{!! Widget::email('email', 'E-Mail Address')->required()->autofocus() !!}
<div class="col-md-6"> {!! Widget::password('password', 'Password')->required() !!}
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required autofocus> {!! Widget::checkbox('remember', 'Remember Me')->checked(false) !!}
@if ($errors->has('email')) <div class="form-group row mb-0">
<span class="invalid-feedback" role="alert"> <div class="col-md-8 offset-md-4">
<strong>{{ $errors->first('email') }}</strong> <button type="submit" class="btn btn-primary">{{ __('Login') }}</button>{{--
</span> --}}<a class="btn btn-link" href="{{ route('password.request') }}">{{ __('Forgot Your Password?') }}</a>{{--
@endif --}}<a class="btn btn-link pl-0" href="{{ route('register') }}">{{ __('Register') }}</a>
</div> </div>
</div> </div>
</form>
<div class="form-group row"> </div>
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="card-footer bg-white">
<div class="col-md-6"> <div class="form-group row mb-0">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required> <span class="col-md-4 col-form-label text-md-right text-muted">…or authenticate with</span>
@if ($errors->has('password')) <div class="col-md-6">
<span class="invalid-feedback" role="alert"> @set('services.oauth_providers.github.client_id')
<strong>{{ $errors->first('password') }}</strong> <a type="submit" href="{{route('oauth-github-authorize')}}" class="btn btn-dark">
</span> <i class="fa-github pr-1"></i>
@endif {{ __('GitHub') }}
</div> </a>
</div> @endset
<div class="form-group row"> @set('services.oauth_providers.google.client_id')
<div class="col-md-6 offset-md-4"> <a type="submit" href="{{route('oauth-google-authorize')}}" class="btn btn-dark">
<div class="checkbox"> <i class="fa-google pr-1"></i>
<label> {{ __('Google') }}
<input type="checkbox" name="remember" {{ old('remember') ? 'checked' : '' }}> {{ __('Remember Me') }} </a>
</label> @endset
</div>
</div> @set('services.oauth_providers.facebook.client_id')
</div> <a type="submit" href="{{route('oauth-facebook-authorize')}}" class="btn btn-dark">
<i class="fa-facebook-square pr-1"></i>
<div class="form-group row mb-0"> {{ __('Facebook') }}
<div class="col-md-8 offset-md-4"> </a>
<button type="submit" class="btn btn-primary"> @endset
{{ __('Login') }} </div>
</button>{{--
--}}<a class="btn btn-link" href="{{ route('password.request') }}">
{{ __('Forgot Your Password?') }}
</a>{{--
--}}<a class="btn btn-link pl-0" href="{{ route('register') }}">
{{ __('Register') }}
</a>
</div>
</div>
</form>
</div>
<div class="card-footer bg-white">
<div class="form-group row mb-0">
<span class="col-md-2 col-form-label text-md-right">{{ __('Login with') }}</span>
<div class="col-md-8">
@set('services.oauth_providers.github.client_id')
<a type="submit" href="{{route('oauth-github-authorize')}}" class="btn btn-dark">
<i class="fa-github pr-1"></i>
{{ __('GitHub') }}
</a>
@endset
@set('services.oauth_providers.google.client_id')
<a type="submit" href="{{route('oauth-google-authorize')}}" class="btn btn-dark">
<i class="fa-google pr-1"></i>
{{ __('Google') }}
</a>
@endset
@set('services.oauth_providers.facebook.client_id')
<a type="submit" href="{{route('oauth-facebook-authorize')}}" class="btn btn-dark">
<i class="fa-facebook-square pr-1"></i>
{{ __('Facebook') }}
</a>
@endset
</div>
</div>
</div>
</div> </div>
</div>
</div> </div>
</div>
</div> </div>
</div> </div>
@endsection @endsection

@ -11,55 +11,16 @@
<form method="POST" action="{{ route('register') }}" aria-label="{{ __('Register') }}"> <form method="POST" action="{{ route('register') }}" aria-label="{{ __('Register') }}">
@csrf @csrf
<div class="form-group row"> @php(Widget::setLayout(4, 6))
<label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>
<div class="col-md-6"> {!! Widget::text('name', 'Name')->required()->autofocus()
<input id="name" type="text" class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}" name="name" value="{{ old('name') }}" required autofocus> ->help('This will be part of your vanity URL; only letters, digits and
some symbols are allowed. You can always change this later, and even
set a different Display Name.') !!}
@if ($errors->has('name')) {!! Widget::email('email', 'E-Mail Address')->required()->autofocus() !!}
<span class="invalid-feedback" role="alert"> {!! Widget::password('password', 'Password')->required() !!}
<strong>{{ $errors->first('name') }}</strong> {!! Widget::password('password_confirmation', 'Confirm Password')->required() !!}
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required>
@if ($errors->has('email'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>
@if ($errors->has('password'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
</div>
</div>
<div class="form-group row mb-0"> <div class="form-group row mb-0">
<div class="col-md-6 offset-md-4"> <div class="col-md-6 offset-md-4">

@ -1,6 +1,6 @@
<div class="col-md-1 pl-0"> <div class="col-md-1 pl-0">
@if($w->help) @if($w->help)
<i class="fa-question-circle" <i class="fa-question-circle form-help"
data-toggle="tooltip" data-toggle="tooltip"
data-placement="right" data-placement="right"
@if(false!==strpos($w->help, '<')) @if(false!==strpos($w->help, '<'))

@ -0,0 +1,22 @@
@php
/** @var \App\View\Widget $w */
@endphp
<div class="row form-group">
<div class="col-md-{{$w->fieldCols}} offset-md-{{$w->labelCols}}">
<div class="custom-control custom-checkbox{{ $errors->has($w->name) ? ' is-invalid' : '' }}">
<input type="checkbox" class="custom-control-input"
id="field-{{ $w->name }}"
{{+$w->value?'checked':''}}
name="{{ $w->name }}">
<label class="custom-control-label" for="field-{{ $w->name }}">{{ $w->label }}</label>
</div>
@if ($errors->has($w->name))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first($w->name) }}</strong>
</span>
@endif
</div>
@include('form._help')
</div>

@ -3,8 +3,8 @@
@endphp @endphp
<div class="row form-group"> <div class="row form-group">
<label for="field-{{ $w->name }}" class="col-md-3 col-form-label text-md-right">{{ $w->label }}</label> <label for="field-{{ $w->name }}" class="col-md-{{$w->labelCols}} col-form-label text-md-right">{{ $w->label }}</label>
<div class="col-md-8 pl-0"> <div class="col-md-{{$w->fieldCols}}">
<input id="field-{{ $w->name }}" <input id="field-{{ $w->name }}"
name="{{ $w->name }}" name="{{ $w->name }}"
class="form-control{{ $errors->has($w->name) ? ' is-invalid' : '' }}" class="form-control{{ $errors->has($w->name) ? ' is-invalid' : '' }}"

@ -3,8 +3,8 @@
@endphp @endphp
<div class="row form-group"> <div class="row form-group">
<label for="field-{{ $w->name }}" class="col-md-3 col-form-label text-md-right">{{ $w->label }}</label> <label for="field-{{ $w->name }}" class="col-md-{{$w->labelCols}} col-form-label text-md-right">{{ $w->label }}</label>
<div class="col-md-8 pl-0"> <div class="col-md-{{$w->fieldCols}}">
<textarea id="field-{{ $w->name }}" <textarea id="field-{{ $w->name }}"
name="{{ $w->name }}" name="{{ $w->name }}"
class="form-control{{ $errors->has($w->name) ? ' is-invalid' : '' }}" class="form-control{{ $errors->has($w->name) ? ' is-invalid' : '' }}"

@ -2,14 +2,21 @@
@section('content') @section('content')
<div class="container"> <div class="container">
<form method="POST" action="{{route('table.storeNew')}}" class="row justify-content-center"> <form method="POST" action="{{route('table.storeNew')}}" class="row justify-content-center"
aria-label="New Table">
<h2>New Table</h2> <h2>New Table</h2>
@csrf @csrf
<div class="col-md-8"> <div class="col-md-8">
{!! Widget::text('table-name', 'Title')->help('Unique among your tables') !!} {!! Widget::text('name', 'Name')->value('molluscs-'.uniqid())
->help('Unique among your tables, and part of the URL; only letters, digits and
some symbols are allowed.') !!}
{!! Widget::text('title', 'Title')
->help('Unique among your tables') !!}
{!! Widget::textarea('table-descr', 'Description')->height('8em') {!! Widget::textarea('description', 'Description')->height('8em')
->help('Description what data is in the table. Please use the dedicated ->help('Description what data is in the table. Please use the dedicated
fields for License and data source. URLs in a full format will be clickable.') !!} fields for License and data source. URLs in a full format will be clickable.') !!}
@ -21,24 +28,24 @@
->help('If you took the data from some external site, a book, etc., write it here. ->help('If you took the data from some external site, a book, etc., write it here.
URLs in a full format will be clickable.') !!} URLs in a full format will be clickable.') !!}
{!! Widget::textarea('col-spec', 'Columns')->value($exampleColSpec)->height('8em') {!! Widget::textarea('columns', 'Columns')->value($exampleColumns)->height('8em')
->help(' ->help('
<div class="text-left"> <div class="text-left">
Column parameters in CSV format: Column parameters in CSV format:
<ul class="pl-3 mb-0"> <ul class="pl-3 mb-0">
<li><b>column identifier</b><br>letters, numbers, underscore <li><b>column identifier</b><br>letters, numbers, underscore
<li><b>column data type</b><br>int, string, float, bool <li><b>column data type</b><br>int, string, float, bool
<li><b>column title</b><br>used for display (optional) <li><b>column title</b><br>used for display (optional)
</ul> </ul>
</div>') !!} </div>') !!}
{!! Widget::textarea('row-data', 'Initial data')->value($exampleData)->height('12em') {!! Widget::textarea('data', 'Initial data')->value($exampleData)->height('12em')
->help(' ->help('
Initial table data in CSV format, columns corresponding to the Initial table data in CSV format, columns corresponding to the
specification you entered above.') !!} specification you entered above.') !!}
<div class="row form-group"> <div class="row form-group">
<div class="col-md-8 offset-md-3 pl-0"> <div class="col-md-8 offset-md-3">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="fa-save pr-2"></i>Create New Table <i class="fa-save pr-2"></i>Create New Table
</button> </button>

Loading…
Cancel
Save