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 489 additions and 18 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');
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/dist/all.js vendored

Binary file not shown.

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',