Feature/custom fields default values (#5389)

* Fixes CustomFieldsetsController::fields() which I think is not used anywhere else and don't think ever worked as you can't call get() on a Collection.
Have tested extensively and doesn't seem to affect anywhere else?

* Adds default value functionality

* Adds built assets

* Fixes assignment to asset_model_id which should have been evaluation and alters route so it sits more in line with existing work

* Updates built assets

* Remove silly docker.env file; fix Dockerfile to preserve Oauth keys (#5377)

* Added department to custom asset export
Updates build assets

* Adds translation support for 'add default values' checkbox label
This commit is contained in:
Hannah Tinkler 2018-04-24 05:16:55 +01:00 committed by snipe
parent 132a5d424d
commit 8d501e1c24
22 changed files with 548 additions and 77 deletions

View file

@ -156,8 +156,26 @@ class CustomFieldsetsController extends Controller
{ {
$this->authorize('view', CustomFieldset::class); $this->authorize('view', CustomFieldset::class);
$set = CustomFieldset::findOrFail($id); $set = CustomFieldset::findOrFail($id);
$fields = $set->fields->get(); $fields = $set->fields;
return (new CustomFieldsTransformer)->transformCustomFields($fields, $fields->count()); return (new CustomFieldsTransformer)->transformCustomFields($fields, $fields->count());
} }
/**
* Return JSON containing a list of fields belonging to a fieldset with the
* default values for a given model
*
* @param $modelId
* @param $fieldsetId
* @return string JSON
*/
public function fieldsWithDefaultValues($fieldsetId, $modelId)
{
$this->authorize('view', CustomFieldset::class);
$set = CustomFieldset::findOrFail($fieldsetId);
$fields = $set->fields;
return (new CustomFieldsTransformer)->transformCustomFieldsWithDefaultValues($fields, $modelId, $fields->count());
}
} }

View file

@ -110,6 +110,10 @@ class AssetModelsController extends Controller
// Was it created? // Was it created?
if ($model->save()) { if ($model->save()) {
if ($this->shouldAddDefaultValues($request->input())) {
$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'));
}
// Redirect to the new model page // Redirect to the new model page
return redirect()->route("models.index")->with('success', trans('admin/models/message.create.success')); return redirect()->route("models.index")->with('success', trans('admin/models/message.create.success'));
} }
@ -206,10 +210,16 @@ class AssetModelsController extends Controller
$model->notes = $request->input('notes'); $model->notes = $request->input('notes');
$model->requestable = $request->input('requestable', '0'); $model->requestable = $request->input('requestable', '0');
$this->removeCustomFieldsDefaultValues($model);
if ($request->input('custom_fieldset')=='') { if ($request->input('custom_fieldset')=='') {
$model->fieldset_id = null; $model->fieldset_id = null;
} else { } else {
$model->fieldset_id = $request->input('custom_fieldset'); $model->fieldset_id = $request->input('custom_fieldset');
if ($this->shouldAddDefaultValues($request->input())) {
$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'));
}
} }
$old_image = $model->image; $old_image = $model->image;
@ -531,4 +541,43 @@ class AssetModelsController extends Controller
} }
/**
* Returns true if a fieldset is set, 'add default values' is ticked and if
* any default values were entered into the form.
*
* @param array $input
* @return boolean
*/
private function shouldAddDefaultValues(array $input)
{
return !empty($input['add_default_values'])
&& !empty($input['default_values'])
&& !empty($input['custom_fieldset']);
}
/**
* Adds default values to a model (as long as they are truthy)
*
* @param AssetModel $model
* @param array $defaultValues
* @return void
*/
private function assignCustomFieldsDefaultValues(AssetModel $model, array $defaultValues)
{
foreach ($defaultValues as $customFieldId => $defaultValue) {
if ($defaultValue) {
$model->defaultValues()->attach($customFieldId, ['default_value' => $defaultValue]);
}
}
}
/**
* Removes all default values
*
* @return void
*/
private function removeCustomFieldsDefaultValues(AssetModel $model)
{
$model->defaultValues()->detach();
}
} }

View file

@ -18,15 +18,34 @@ class CustomFieldsTransformer
return (new DatatablesTransformer)->transformDatatables($array, $total); return (new DatatablesTransformer)->transformDatatables($array, $total);
} }
/**
* Builds up an array of formatted custom fields
* @param Collection $fields
* @param int $modelId
* @param int $total
* @return array
*/
public function transformCustomFieldsWithDefaultValues (Collection $fields, $modelId, $total)
{
$array = [];
foreach ($fields as $field) {
$array[] = self::transformCustomFieldWithDefaultValue($field, $modelId);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformCustomField (CustomField $field) public function transformCustomField (CustomField $field)
{ {
$array = [ $array = [
'id' => $field->id, 'id' => $field->id,
'name' => e($field->name), 'name' => e($field->name),
'db_column_name' => e($field->db_column_name()), 'db_column_name' => e($field->db_column_name()),
'format' => e($field->format), 'format' => e($field->format),
'field_values' => ($field->field_values) ? e($field->field_values) : null, 'field_values' => ($field->field_values) ? e($field->field_values) : null,
'field_values_array' => ($field->field_values) ? explode("\r\n", e($field->field_values)) : null,
'type' => e($field->element),
'required' => $field->pivot ? $field->pivot->required : false, 'required' => $field->pivot ? $field->pivot->required : false,
'created_at' => Helper::getFormattedDateObject($field->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($field->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($field->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($field->updated_at, 'datetime'),
@ -34,5 +53,22 @@ class CustomFieldsTransformer
return $array; return $array;
} }
/**
* Returns the core data for a field, including the default value it has
* when attributed to a certain model
*
* @param CustomField $field
* @param int $modelId
* @return array
*/
public function transformCustomFieldWithDefaultValue (CustomField $field, $modelId)
{
return [
'id' => $field->id,
'name' => e($field->name),
'type' => e($field->element),
'field_values_array' => ($field->field_values) ? explode("\r\n", e($field->field_values)) : null,
'default_value' => $field->defaultValue($modelId),
];
}
} }

View file

@ -98,6 +98,11 @@ class AssetModel extends SnipeModel
return $this->belongsTo('\App\Models\CustomFieldset', 'fieldset_id'); return $this->belongsTo('\App\Models\CustomFieldset', 'fieldset_id');
} }
public function defaultValues()
{
return $this->belongsToMany('\App\Models\CustomField', 'models_custom_fields')->withPivot('default_value');
}
public function getImageUrl() { public function getImageUrl() {
if ($this->image) { if ($this->image) {

View file

@ -146,6 +146,26 @@ class CustomField extends Model
return $this->belongsTo('\App\Models\User'); return $this->belongsTo('\App\Models\User');
} }
public function defaultValues()
{
return $this->belongsToMany('\App\Models\AssetModel', 'models_custom_fields')->withPivot('default_value');
}
/**
* Returns the default value for a given model using the defaultValues
* relationship
*
* @param int $modelId
* @return string
*/
public function defaultValue($modelId)
{
return $this->defaultValues->filter(function ($item) use ($modelId) {
return $item->pivot->asset_model_id == $modelId;
})->map(function ($item) {
return $item->pivot->default_value;
})->first();
}
public function check_format($value) public function check_format($value)
{ {

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCustomFieldDefaultValuesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('models_custom_fields', function (Blueprint $table) {
$table->increments('id');
$table->integer('asset_model_id');
$table->integer('custom_field_id');
$table->text('default_value')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('models_custom_fields');
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

52
public/js/dist/all.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,14 +1,14 @@
{ {
"/js/build/vue.js": "/js/build/vue.js?id=cd8def41b04c6707fc9e", "/js/build/vue.js": "/js/build/vue.js?id=88921ad9bb64a0915ebb",
"/css/AdminLTE.css": "/css/AdminLTE.css?id=b8be19a285eaf44eec37", "/css/AdminLTE.css": "/css/AdminLTE.css?id=5e72463a66acbcc740d5",
"/css/app.css": "/css/app.css?id=407edb63cc6b6dc62405", "/css/app.css": "/css/app.css?id=407edb63cc6b6dc62405",
"/css/overrides.css": "/css/overrides.css?id=c289c71c08df753ebc45", "/css/overrides.css": "/css/overrides.css?id=c289c71c08df753ebc45",
"/js/build/vue.js.map": "/js/build/vue.js.map?id=ae61f2fa91bc184b92a9", "/js/build/vue.js.map": "/js/build/vue.js.map?id=0b7679d18eb22094e3b7",
"/css/AdminLTE.css.map": "/css/AdminLTE.css.map?id=99f5a5a03c4155cf69f6", "/css/AdminLTE.css.map": "/css/AdminLTE.css.map?id=99f5a5a03c4155cf69f6",
"/css/app.css.map": "/css/app.css.map?id=bdbe05e6ecd70ccfac72", "/css/app.css.map": "/css/app.css.map?id=bdbe05e6ecd70ccfac72",
"/css/overrides.css.map": "/css/overrides.css.map?id=898c91d4a425b01b589b", "/css/overrides.css.map": "/css/overrides.css.map?id=898c91d4a425b01b589b",
"/css/dist/all.css": "/css/dist/all.css?id=5fdad90c2d445e4a1a2c", "/css/dist/all.css": "/css/dist/all.css?id=e3ae07b03a1d53657a1e",
"/js/dist/all.js": "/js/dist/all.js?id=dc00b6ea982000d41b0e", "/js/dist/all.js": "/js/dist/all.js?id=3f7017ebedf1da0319ef",
"/css/build/all.css": "/css/build/all.css?id=5fdad90c2d445e4a1a2c", "/css/build/all.css": "/css/build/all.css?id=e3ae07b03a1d53657a1e",
"/js/build/all.js": "/js/build/all.js?id=dc00b6ea982000d41b0e" "/js/build/all.js": "/js/build/all.js?id=3f7017ebedf1da0319ef"
} }

View file

@ -0,0 +1,218 @@
<style scoped>
legend {
font-size: 13px;
font-weight: bold;
border: 0;
}
fieldset > div {
background: #f4f4f4;
border: 1px solid #d3d6de;
margin: 0 15px 15px;
padding: 20px 20px 10px;
}
@media (max-width: 992px) {
legend {
text-align: left !important;
}
}
@media (min-width: 992px) {
fieldset > div {
width: 55%;
}
}
</style>
<template>
<div>
<div v-if="show && fields.length">
<div class="form-group">
<fieldset>
<legend class="col-md-3 control-label">Default Values</legend>
<div class="col-sm-8 col-xl-7">
<p v-if="error">
There was a problem retrieving the fields for this fieldset.
</p>
<div class="row" v-for="field in fields">
<div class="col-sm-12 col-lg-6">
<label class="control-label" :for="'default-value' + field.id">{{ field.name }}</label>
</div>
<div class="col-sm-12 col-lg-6">
<input v-if="field.type == 'text'" class="form-control m-b-xs" type="text" :value="getValue(field)" :id="'default-value' + field.id" :name="'default_values[' + field.id + ']'">
<select v-if="field.type == 'listbox'" class="form-control m-b-xs" :name="'default_values[' + field.id + ']'">
<option value="0"></option>
<option v-for="field_value in field.field_values_array" :value="field_value" :selected="getValue(field) == field_value">{{ field_value }}</option>
</select>
</div>
</div>
</div>
</fieldset>
</div>
</div>
</div>
</template>
<script>
export default {
props: [
'fieldsetId',
'modelId',
'previousInput',
],
data() {
return {
identifiers: {
fieldset: null,
model: null,
},
elements: {
fieldset: null,
field: null,
},
fields: null,
show: false,
error: false,
}
},
/**
* Initialise the component (Vue 1.x).
*/
ready() {
this.init()
},
/**
* Initialise the component (Vue 2.x).
*/
mounted() {
this.init()
},
methods: {
/**
* Grabs the toggle field and connected fieldset and if present,
* set up the rest of the component. Scope lookups to the component
* only so we're not traversing and/or manipulating the whole DOM
*/
init() {
this.defaultValues = JSON.parse(this.previousInput);
this.identifiers.fieldset = this.fieldsetId
this.identifiers.model = this.modelId
// This has to be jQuery because a lot of native functions/events
// do not work with select2
this.elements.fieldset = $('.js-fieldset-field')
this.elements.field = document.querySelector('.js-default-values-toggler')
if (this.elements.fieldset && this.elements.field) {
this.addListeners()
this.getFields()
}
},
/**
* Adds event listeners for:
* - Toggle field changing
* - Fieldset field changing
*
* Using jQuery event hooks for the select2 fieldset field as
* select2 does not emit DOM events...
*/
addListeners() {
this.elements.field.addEventListener('change', e => this.updateShow())
this.elements.fieldset.on('change', e => this.updateFields())
},
/**
* Call the CustomFieldsetsController::fields() endpoint to grab
* the fields we can set default values for
*/
getFields() {
if (!this.identifiers.fieldset) {
return this.fields = [];
}
this.$http.get(this.getUrl())
.then(response => response.json())
.then(data => this.checkResponseForError(data))
.then(data => this.fields = data.rows)
.then(() => this.determineIfShouldShow())
},
getValue(field) {
if (field.default_value) {
return field.default_value
}
return this.defaultValues != null ? this.defaultValues[field.id.toString()] : ''
},
/**
* Generates the API URL depending on what information is available
*
* @return Router
*/
getUrl() {
if (this.identifiers.model) {
return route('api.fieldsets.fields-with-default-value', {
fieldset: this.identifiers.fieldset,
model: this.identifiers.model,
})
}
return route('api.fieldsets.fields', {
fieldset: this.identifiers.fieldset,
})
},
/**
* Sets error state and shows error if request was not marked
* successful
*/
checkResponseForError(data) {
this.error = data.status == 'error'
return data
},
/**
* Checks whether the toggler is checked and shows the default
* values field dependent on that
*/
updateShow() {
if (this.identifiers.fieldset && this.elements.field) {
this.show = this.elements.field.checked
}
},
/**
* checks the 'add default values' checkbox if it is already checked
* OR this.show is already set to true OR if any fields already have
* a default value.
*/
determineIfShouldShow() {
this.elements.field.checked = this.elements.field.checked
|| this.show
|| this.fields.reduce((accumulator, currentValue) => {
return accumulator || currentValue.default_value
}, false)
this.updateShow()
},
updateFields() {
this.identifiers.fieldset = this.elements.fieldset[0].value ? parseInt(this.elements.fieldset[0].value) : false
this.getFields()
},
}
}
</script>

View file

@ -32,6 +32,11 @@ Vue.component(
require('./components/importer/importer.vue') require('./components/importer/importer.vue')
); );
Vue.component(
'fieldset-default-values',
require('./components/forms/asset-models/fieldset-default-values.vue')
);
// Commented out currently to avoid trying to load vue everywhere. // Commented out currently to avoid trying to load vue everywhere.
// const app = new Vue({ // const app = new Vue({
// el: '#app' // el: '#app'

View file

@ -39,6 +39,10 @@
@import "labels.less"; @import "labels.less";
@import "modal.less"; @import "modal.less";
//HELPERS
//-----------
@import "spacing.less";
//PAGES //PAGES
//------ //------
@import "login_and_register.less"; @import "login_and_register.less";

View file

@ -0,0 +1,56 @@
/*
* Helpers: Spacing
* Universal minor spacing classes to help space things out without
* use-dedicated classes
* -----------------
*/
@props: margin m, padding p;
@spacers: xs 5px 10px,
sm 10px 20px,
md 20px 30px;
.loop-props(@prop-index) when (@prop-index > 0){
@prop: extract(@props, @prop-index);
@prop-name: extract(@prop, 1);
@abbrev: extract(@prop, 2);
.loop-sizes(@prop-name; @abbrev; length(@spacers));
.loop-props(@prop-index - 1);
}
.loop-props(length(@props)) !important;
.loop-sizes(@prop-name; @abbrev; @size-index) when (@size-index > 0){
@spacer: extract(@spacers, @size-index);
@size: extract(@spacer, 1);
@x: extract(@spacer, 2);
@y: extract(@spacer, 3);
.@{abbrev}-a-@{size} {
@{prop-name}: @y @x;
}
.@{abbrev}-t-@{size} {
@{prop-name}-top: @y;
}
.@{abbrev}-r-@{size} {
@{prop-name}-right: @x;
}
.@{abbrev}-b-@{size} {
@{prop-name}-bottom: @y;
}
.@{abbrev}-l-@{size} {
@{prop-name}-left: @x;
}
.@{abbrev}-x-@{size} {
@{prop-name}-right: @x;
@{prop-name}-left: @x;
}
.@{abbrev}-y-@{size} {
@{prop-name}-top: @y;
@{prop-name}-bottom: @y;
}
.loop-sizes(@prop-name; @abbrev; @size-index - 1);
}

View file

@ -14,5 +14,5 @@ return array(
'view_models' => 'View Models', 'view_models' => 'View Models',
'fieldset' => 'Fieldset', 'fieldset' => 'Fieldset',
'no_custom_field' => 'No custom fields', 'no_custom_field' => 'No custom fields',
'add_default_values' => 'Add default values',
); );

View file

@ -8,7 +8,7 @@
<!-- Listbox --> <!-- Listbox -->
@if ($field->element=='listbox') @if ($field->element=='listbox')
{{ Form::select($field->db_column_name(), $field->formatFieldValuesAsArray(), {{ Form::select($field->db_column_name(), $field->formatFieldValuesAsArray(),
Input::old($field->db_column_name(),(isset($item) ? $item->{$field->db_column_name()} : "")), ['class'=>'format select2 form-control']) }} Input::old($field->db_column_name(),(isset($item) ? $item->{$field->db_column_name()} : $field->defaultValue($model->id))), ['class'=>'format select2 form-control']) }}
@elseif ($field->element=='checkbox') @elseif ($field->element=='checkbox')
<!-- Checkboxes --> <!-- Checkboxes -->
@ -39,7 +39,7 @@
@else @else
@if (($field->field_encrypted=='0') || (Gate::allows('admin'))) @if (($field->field_encrypted=='0') || (Gate::allows('admin')))
<input type="text" value="{{ Input::old($field->db_column_name(),(isset($item) ? \App\Helpers\Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : "")) }}" id="{{ $field->db_column_name() }}" class="form-control" name="{{ $field->db_column_name() }}" placeholder="Enter {{ strtolower($field->format) }} text"> <input type="text" value="{{ Input::old($field->db_column_name(),(isset($item) ? \App\Helpers\Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))) }}" id="{{ $field->db_column_name() }}" class="form-control" name="{{ $field->db_column_name() }}" placeholder="Enter {{ strtolower($field->format) }} text">
@else @else
<input type="text" value="{{ strtoupper(trans('admin/custom_fields/general.encrypted')) }}" class="form-control disabled" disabled> <input type="text" value="{{ strtoupper(trans('admin/custom_fields/general.encrypted')) }}" class="form-control disabled" disabled>
@endif @endif

View file

@ -33,12 +33,25 @@
</div> </div>
<!-- Custom Fieldset --> <!-- Custom Fieldset -->
<div class="form-group {{ $errors->has('custom_fieldset') ? ' has-error' : '' }}"> <div id="app">
<label for="custom_fieldset" class="col-md-3 control-label">{{ trans('admin/models/general.fieldset') }}</label> <div class="form-group {{ $errors->has('custom_fieldset') ? ' has-error' : '' }}">
<div class="col-md-7"> <label for="custom_fieldset" class="col-md-3 control-label">{{ trans('admin/models/general.fieldset') }}</label>
{{ Form::select('custom_fieldset', \App\Helpers\Helper::customFieldsetList(),Input::old('custom_fieldset', $item->fieldset_id), array('class'=>'select2', 'style'=>'width:350px')) }} <div class="col-md-7">
{!! $errors->first('custom_fieldset', '<span class="alert-msg"><br><i class="fa fa-times"></i> :message</span>') !!} {{ Form::select('custom_fieldset', \App\Helpers\Helper::customFieldsetList(),Input::old('custom_fieldset', $item->fieldset_id), array('class'=>'select2 js-fieldset-field', 'style'=>'width:350px')) }}
{!! $errors->first('custom_fieldset', '<span class="alert-msg"><br><i class="fa fa-times"></i> :message</span>') !!}
<label class="m-l-xs">
{{ Form::checkbox('add_default_values', 1, Input::old('add_default_values'), ['class' => 'js-default-values-toggler']) }}
{{ trans('admin/models/general.add_default_values') }}
</label>
</div>
</div> </div>
<fieldset-default-values
model-id="{{ $item->id ?: '' }}"
fieldset-id="{{ !empty($item->fieldset) ? $item->fieldset->id : Input::old('custom_fieldset') }}"
previous-input="{{ json_encode(Input::old('default_values')) }}"
>
</fieldset-default-values>
</div> </div>
@include ('partials.forms.edit.notes') @include ('partials.forms.edit.notes')
@ -59,3 +72,11 @@
@include ('partials.forms.edit.image-upload') @include ('partials.forms.edit.image-upload')
@stop @stop
@section('moar_scripts')
<script nonce="{{ csrf_token() }}">
new Vue({
el: '#app'
});
</script>
@endsection

View file

@ -259,6 +259,12 @@ Route::group(['prefix' => 'v1','namespace' => 'Api'], function () {
'uses' => 'CustomFieldsetsController@fields' 'uses' => 'CustomFieldsetsController@fields'
] ]
); );
Route::get('/{fieldset}/fields/{model}',
[
'as' => 'api.fieldsets.fields-with-default-value',
'uses' => 'CustomFieldsetsController@fieldsWithDefaultValues'
]
);
}); });
Route::resource('fieldsets', 'CustomFieldsetsController', Route::resource('fieldsets', 'CustomFieldsetsController',