From 096fb7a116f7bafb2cc9c768bf3708709f4440b6 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 18 Sep 2019 16:18:15 +0200 Subject: [PATCH] Implement custom entities / objects (closes #242) --- changelog/52_UNRELEASED_2019-xx-xx.md | 5 ++ controllers/BaseController.php | 21 +++----- controllers/GenericEntityController.php | 59 +++++++++++++++++++++ grocy.openapi.json | 2 + localization/demo_data.pot | 12 +++++ localization/strings.pot | 30 +++++++++++ migrations/0085.sql | 17 ++++++ public/viewjs/userentities.js | 69 +++++++++++++++++++++++++ public/viewjs/userentityform.js | 68 ++++++++++++++++++++++++ public/viewjs/userobjectform.js | 47 +++++++++++++++++ public/viewjs/userobjects.js | 68 ++++++++++++++++++++++++ routes.php | 4 ++ services/ApplicationService.php | 15 +++++- services/DemoDataGeneratorService.php | 13 +++++ services/UserfieldsService.php | 12 ++++- views/layout/default.blade.php | 17 ++++++ views/userentities.blade.php | 62 ++++++++++++++++++++++ views/userentityform.blade.php | 59 +++++++++++++++++++++ views/userobjectform.blade.php | 38 ++++++++++++++ views/userobjects.blade.php | 66 +++++++++++++++++++++++ 20 files changed, 666 insertions(+), 18 deletions(-) create mode 100644 migrations/0085.sql create mode 100644 public/viewjs/userentities.js create mode 100644 public/viewjs/userentityform.js create mode 100644 public/viewjs/userobjectform.js create mode 100644 public/viewjs/userobjects.js create mode 100644 views/userentities.blade.php create mode 100644 views/userentityform.blade.php create mode 100644 views/userobjectform.blade.php create mode 100644 views/userobjects.blade.php diff --git a/changelog/52_UNRELEASED_2019-xx-xx.md b/changelog/52_UNRELEASED_2019-xx-xx.md index 525ac354..48b7d2fc 100644 --- a/changelog/52_UNRELEASED_2019-xx-xx.md +++ b/changelog/52_UNRELEASED_2019-xx-xx.md @@ -1,3 +1,8 @@ +### New feature: Custom entities / objects +- Custom entities are based on Userfields and can be used to add any custom lists you want to have in grocy +- They can have an own menu entry in the sidebar +- => See "Manage master data" -> Userentities or try it on the demo: https://demo.grocy.info/userobjects/exampleuserentity + ### Stock improvements - Products can now have variations (nested products) - Define the parent product for a product on the product edit page (only one level is possible, means a product which is used as a parent product in another product, cannot have a parent product itself) diff --git a/controllers/BaseController.php b/controllers/BaseController.php index f48c2661..0d6349d5 100644 --- a/controllers/BaseController.php +++ b/controllers/BaseController.php @@ -16,21 +16,10 @@ class BaseController $localizationService = new LocalizationService(GROCY_CULTURE); $this->LocalizationService = $localizationService; - if (GROCY_MODE === 'prerelease') - { - $commitHash = trim(exec('git log --pretty="%h" -n1 HEAD')); - $commitDate = trim(exec('git log --date=iso --pretty="%cd" -n1 HEAD')); - - $container->view->set('version', "pre-release-$commitHash"); - $container->view->set('releaseDate', \substr($commitDate, 0, 19)); - } - else - { - $applicationService = new ApplicationService(); - $versionInfo = $applicationService->GetInstalledVersion(); - $container->view->set('version', $versionInfo->Version); - $container->view->set('releaseDate', $versionInfo->ReleaseDate); - } + $applicationService = new ApplicationService(); + $versionInfo = $applicationService->GetInstalledVersion(); + $container->view->set('version', $versionInfo->Version); + $container->view->set('releaseDate', $versionInfo->ReleaseDate); $container->view->set('__t', function(string $text, ...$placeholderValues) use($localizationService) { @@ -64,6 +53,8 @@ class BaseController } $container->view->set('featureFlags', $constants); + $container->view->set('userentitiesForSidebar', $this->Database->userentities()->where('show_in_sidebar_menu = 1')->orderBy('name')); + try { $usersService = new UsersService(); diff --git a/controllers/GenericEntityController.php b/controllers/GenericEntityController.php index ed1956c6..65c5cd1d 100644 --- a/controllers/GenericEntityController.php +++ b/controllers/GenericEntityController.php @@ -22,6 +22,25 @@ class GenericEntityController extends BaseController ]); } + public function UserentitiesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + return $this->AppContainer->view->render($response, 'userentities', [ + 'userentities' => $this->Database->userentities()->orderBy('name') + ]); + } + + public function UserobjectsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + $userentity = $this->Database->userentities()->where('name = :1', $args['userentityName'])->fetch(); + + return $this->AppContainer->view->render($response, 'userobjects', [ + 'userentity' => $userentity, + 'userobjects' => $this->Database->userobjects()->where('userentity_id = :1', $userentity->id), + 'userfields' => $this->UserfieldsService->GetFields('userentity-' . $args['userentityName']), + 'userfieldValues' => $this->UserfieldsService->GetAllValues('userentity-' . $args['userentityName']) + ]); + } + public function UserfieldEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { if ($args['userfieldId'] == 'new') @@ -42,4 +61,44 @@ class GenericEntityController extends BaseController ]); } } + + public function UserentityEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + if ($args['userentityId'] == 'new') + { + return $this->AppContainer->view->render($response, 'userentityform', [ + 'mode' => 'create' + ]); + } + else + { + return $this->AppContainer->view->render($response, 'userentityform', [ + 'mode' => 'edit', + 'userentity' => $this->Database->userentities($args['userentityId']) + ]); + } + } + + public function UserobjectEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + $userentity = $this->Database->userentities()->where('name = :1', $args['userentityName'])->fetch(); + + if ($args['userobjectId'] == 'new') + { + return $this->AppContainer->view->render($response, 'userobjectform', [ + 'userentity' => $userentity, + 'mode' => 'create', + 'userfields' => $this->UserfieldsService->GetFields('userentity-' . $args['userentityName']) + ]); + } + else + { + return $this->AppContainer->view->render($response, 'userobjectform', [ + 'userentity' => $userentity, + 'mode' => 'edit', + 'userobject' => $this->Database->userobjects($args['userobjectId']), + 'userfields' => $this->UserfieldsService->GetFields('userentity-' . $args['userentityName']) + ]); + } + } } diff --git a/grocy.openapi.json b/grocy.openapi.json index ad023943..cea3794d 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -2515,6 +2515,8 @@ "equipment", "api_keys", "userfields", + "userentities", + "userobjects", "meal_plan" ] }, diff --git a/localization/demo_data.pot b/localization/demo_data.pot index 0a1eb9bb..75978ada 100644 --- a/localization/demo_data.pot +++ b/localization/demo_data.pot @@ -288,3 +288,15 @@ msgid "Slice" msgid_plural "Slices" msgstr[0] "" msgstr[1] "" + +msgid "Example userentity" +msgstr "" + +msgid "This is an example user entity..." +msgstr "" + +msgid "Custom field" +msgstr "" + +msgid "Example field value..." +msgstr "" diff --git a/localization/strings.pot b/localization/strings.pot index d702bea8..72aaff2b 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1420,3 +1420,33 @@ msgstr "" msgid "Consume product on chore execution" msgstr "" + +msgid "Are you sure to delete user field \"%s\"?" +msgstr "" + +msgid "Userentities" +msgstr "" + +msgid "Create userentity" +msgstr "" + +msgid "Show in sidebar menu" +msgstr "" + +msgid "Edit userentity" +msgstr "" + +msgid "Edit %s" +msgstr "" + +msgid "Create %s" +msgstr "" + +msgid "Are you sure to delete this userobject?" +msgstr "" + +msgid "Icon CSS class" +msgstr "" + +msgid "For example" +msgstr "" diff --git a/migrations/0085.sql b/migrations/0085.sql new file mode 100644 index 00000000..17a8031c --- /dev/null +++ b/migrations/0085.sql @@ -0,0 +1,17 @@ +CREATE TABLE userentities ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + name TEXT NOT NULL, + caption TEXT NOT NULL, + description TEXT, + show_in_sidebar_menu TINYINT NOT NULL DEFAULT 1, + icon_css_class TEXT, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')), + + UNIQUE(name) +); + +CREATE TABLE userobjects ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + userentity_id INTEGER NOT NULL, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) +); diff --git a/public/viewjs/userentities.js b/public/viewjs/userentities.js new file mode 100644 index 00000000..2ea3ddf2 --- /dev/null +++ b/public/viewjs/userentities.js @@ -0,0 +1,69 @@ +var userentitiesTable = $('#userentities-table').DataTable({ + 'paginate': false, + 'order': [[1, 'asc']], + 'columnDefs': [ + { 'orderable': false, 'targets': 0 } + ], + 'language': JSON.parse(__t('datatables_localization')), + 'scrollY': false, + 'colReorder': true, + 'stateSave': true, + 'stateSaveParams': function(settings, data) + { + data.search.search = ""; + + data.columns.forEach(column => + { + column.search.search = ""; + }); + } +}); +$('#userentities-table tbody').removeClass("d-none"); +userentitiesTable.columns.adjust().draw(); + +$("#search").on("keyup", function() +{ + var value = $(this).val(); + if (value === "all") + { + value = ""; + } + + userentitiesTable.search(value).draw(); +}); + +$(document).on('click', '.userentity-delete-button', function (e) +{ + var objectName = $(e.currentTarget).attr('data-userentity-name'); + var objectId = $(e.currentTarget).attr('data-userentity-id'); + + bootbox.confirm({ + message: __t('Are you sure to delete userentity "%s"?', objectName), + buttons: { + confirm: { + label: __t('Yes'), + className: 'btn-success' + }, + cancel: { + label: __t('No'), + className: 'btn-danger' + } + }, + callback: function(result) + { + if (result === true) + { + Grocy.Api.Delete('objects/userentities/' + objectId, { }, + function(result) + { + window.location.href = U('/userentities'); + }, + function(xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/public/viewjs/userentityform.js b/public/viewjs/userentityform.js new file mode 100644 index 00000000..4c376ceb --- /dev/null +++ b/public/viewjs/userentityform.js @@ -0,0 +1,68 @@ +$('#save-userentity-button').on('click', function(e) +{ + e.preventDefault(); + + var jsonData = $('#userentity-form').serializeJSON(); + Grocy.FrontendHelpers.BeginUiBusy("userentity-form"); + + var redirectUrl = U("/userentities"); + + if (Grocy.EditMode === 'create') + { + Grocy.Api.Post('objects/userentities', jsonData, + function(result) + { + window.location.href = redirectUrl; + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy("userentity-form"); + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } + else + { + Grocy.Api.Put('objects/userentities/' + Grocy.EditObjectId, jsonData, + function(result) + { + window.location.href = redirectUrl; + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy("userentity-form"); + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } +}); + +$('#userentity-form input').keyup(function(event) +{ + Grocy.FrontendHelpers.ValidateForm('userentity-form'); +}); + +$('#userentity-form select').change(function(event) +{ + Grocy.FrontendHelpers.ValidateForm('userentity-form'); +}); + +$('#userentity-form input').keydown(function(event) +{ + if (event.keyCode === 13) //Enter + { + event.preventDefault(); + + if (document.getElementById('userentity-form').checkValidity() === false) //There is at least one validation error + { + return false; + } + else + { + $('#save-userentity-button').click(); + } + } +}); + +$('#name').focus(); +Grocy.FrontendHelpers.ValidateForm('userentity-form'); diff --git a/public/viewjs/userobjectform.js b/public/viewjs/userobjectform.js new file mode 100644 index 00000000..e4cb4081 --- /dev/null +++ b/public/viewjs/userobjectform.js @@ -0,0 +1,47 @@ +$('#save-userobject-button').on('click', function(e) +{ + e.preventDefault(); + + var jsonData = {}; + jsonData.userentity_id = Grocy.EditObjectParentId; + console.log(jsonData); + Grocy.FrontendHelpers.BeginUiBusy("userobject-form"); + + if (Grocy.EditMode === 'create') + { + Grocy.Api.Post('objects/userobjects', jsonData, + function(result) + { + Grocy.EditObjectId = result.created_object_id; + Grocy.Components.UserfieldsForm.Save(function() + { + window.location.href = U('/userobjects/' + Grocy.EditObjectParentName); + }); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy("userobject-form"); + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } + else + { + Grocy.Api.Put('objects/userobjects/' + Grocy.EditObjectId, jsonData, + function(result) + { + Grocy.Components.UserfieldsForm.Save(function() + { + window.location.href = U('/userobjects/' + Grocy.EditObjectParentName); + }); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy("userobject-form"); + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } +}); + +Grocy.Components.UserfieldsForm.Load(); diff --git a/public/viewjs/userobjects.js b/public/viewjs/userobjects.js new file mode 100644 index 00000000..21958fa8 --- /dev/null +++ b/public/viewjs/userobjects.js @@ -0,0 +1,68 @@ +var userobjectsTable = $('#userobjects-table').DataTable({ + 'paginate': false, + 'order': [[1, 'asc']], + 'columnDefs': [ + { 'orderable': false, 'targets': 0 } + ], + 'language': JSON.parse(__t('datatables_localization')), + 'scrollY': false, + 'colReorder': true, + 'stateSave': true, + 'stateSaveParams': function(settings, data) + { + data.search.search = ""; + + data.columns.forEach(column => + { + column.search.search = ""; + }); + } +}); +$('#userobjects-table tbody').removeClass("d-none"); +userobjectsTable.columns.adjust().draw(); + +$("#search").on("keyup", function() +{ + var value = $(this).val(); + if (value === "all") + { + value = ""; + } + + userobjectsTable.search(value).draw(); +}); + +$(document).on('click', '.userobject-delete-button', function (e) +{ + var objectId = $(e.currentTarget).attr('data-userobject-id'); + + bootbox.confirm({ + message: __t('Are you sure to delete this userobject?'), + buttons: { + confirm: { + label: __t('Yes'), + className: 'btn-success' + }, + cancel: { + label: __t('No'), + className: 'btn-danger' + } + }, + callback: function(result) + { + if (result === true) + { + Grocy.Api.Delete('objects/userobjects/' + objectId, { }, + function(result) + { + window.location.reload(); + }, + function(xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/routes.php b/routes.php index fb62cd9c..2766740f 100644 --- a/routes.php +++ b/routes.php @@ -19,6 +19,10 @@ $app->group('', function() // Generic entity interaction $this->get('/userfields', '\Grocy\Controllers\GenericEntityController:UserfieldsList'); $this->get('/userfield/{userfieldId}', '\Grocy\Controllers\GenericEntityController:UserfieldEditForm'); + $this->get('/userentities', '\Grocy\Controllers\GenericEntityController:UserentitiesList'); + $this->get('/userentity/{userentityId}', '\Grocy\Controllers\GenericEntityController:UserentityEditForm'); + $this->get('/userobjects/{userentityName}', '\Grocy\Controllers\GenericEntityController:UserobjectsList'); + $this->get('/userobject/{userentityName}/{userobjectId}', '\Grocy\Controllers\GenericEntityController:UserobjectEditForm'); // User routes $this->get('/users', '\Grocy\Controllers\UsersController:UsersList'); diff --git a/services/ApplicationService.php b/services/ApplicationService.php index 361d676a..27bc842c 100644 --- a/services/ApplicationService.php +++ b/services/ApplicationService.php @@ -9,7 +9,20 @@ class ApplicationService extends BaseService { if ($this->InstalledVersion == null) { - $this->InstalledVersion = json_decode(file_get_contents(__DIR__ . '/../version.json')); + if (GROCY_MODE === 'prerelease') + { + $commitHash = trim(exec('git log --pretty="%h" -n1 HEAD')); + $commitDate = trim(exec('git log --date=iso --pretty="%cd" -n1 HEAD')); + + $this->InstalledVersion = array( + 'Version' => "pre-release-$commitHash", + 'ReleaseDate' => substr($commitDate, 0, 19) + ); + } + else + { + $this->InstalledVersion = json_decode(file_get_contents(__DIR__ . '/../version.json')); + } } return $this->InstalledVersion; diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index 770c96ab..0f7c45ac 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -150,6 +150,19 @@ class DemoDataGeneratorService extends BaseService INSERT INTO equipment (name, description, instruction_manual_file_name) VALUES ('{$this->__t_sql('Coffee machine')}', '{$loremIpsumWithHtmlFormattings}', 'loremipsum.pdf'); --1 INSERT INTO equipment (name, description) VALUES ('{$this->__t_sql('Dishwasher')}', '{$loremIpsumWithHtmlFormattings}'); --2 + INSERT INTO userentities (name, caption, description, show_in_sidebar_menu, icon_css_class) VALUES ('exampleuserentity', '{$this->__t_sql('Example userentity')}', '{$this->__t_sql('This is an example user entity...')}', 1, 'fas fa-smile'); --1 + + INSERT INTO userfields (entity, name, caption, type, show_as_column_in_tables) VALUES ('userentity-exampleuserentity', 'customfield1', '{$this->__t_sql('Custom field')} 1', 'text-single-line', 1); --1 + INSERT INTO userfields (entity, name, caption, type, show_as_column_in_tables) VALUES ('userentity-exampleuserentity', 'customfield2', '{$this->__t_sql('Custom field')} 2', 'text-single-line', 1); --2 + + INSERT INTO userobjects (userentity_id) VALUES (1); --1 + INSERT INTO userobjects (userentity_id) VALUES (1); --2 + + INSERT INTO userfield_values (field_id, object_id, value) VALUES (1, 1, '{$this->__t_sql('Example field value...')}'); + INSERT INTO userfield_values (field_id, object_id, value) VALUES (2, 1, '{$this->__t_sql('Example field value...')}'); + INSERT INTO userfield_values (field_id, object_id, value) VALUES (1, 2, '{$this->__t_sql('Example field value...')}'); + INSERT INTO userfield_values (field_id, object_id, value) VALUES (2, 2, '{$this->__t_sql('Example field value...')}'); + INSERT INTO migrations (migration) VALUES (-1); "; diff --git a/services/UserfieldsService.php b/services/UserfieldsService.php index 5fae50a2..619bc602 100644 --- a/services/UserfieldsService.php +++ b/services/UserfieldsService.php @@ -109,7 +109,15 @@ class UserfieldsService extends BaseService public function GetEntities() { - return $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum; + $exposedDefaultEntities = $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum; + + $userentities = array(); + foreach ($this->Database->userentities()->orderBy('name') as $userentity) + { + $userentities[] = 'userentity-' . $userentity->name; + } + + return array_merge($exposedDefaultEntities, $userentities); } public function GetFieldTypes() @@ -119,6 +127,6 @@ class UserfieldsService extends BaseService private function IsValidEntity($entity) { - return in_array($entity, $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum); + return in_array($entity, $this->GetEntities()); } } diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index 071ce8da..8cd97671 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -186,6 +186,17 @@ @endif + + @php $firstUserentity = true; @endphp + @foreach($userentitiesForSidebar as $userentity) + + @php if ($firstUserentity) { $firstUserentity = false; } @endphp + @endforeach +
  • + + + {{ $__t('Userentities') }} + +
  • diff --git a/views/userentities.blade.php b/views/userentities.blade.php new file mode 100644 index 00000000..2c85f029 --- /dev/null +++ b/views/userentities.blade.php @@ -0,0 +1,62 @@ +@extends('layout.default') + +@section('title', $__t('Userentities')) +@section('activeNav', 'userentities') +@section('viewJsName', 'userentities') + +@section('content') +
    +
    +

    + @yield('title') + +  {{ $__t('Add') }} + +

    +
    +
    + +
    +
    + + +
    +
    + +
    +
    + + + + + + + + + + @foreach($userentities as $userentity) + + + + + + @endforeach + +
    {{ $__t('Name') }}{{ $__t('Caption') }}
    + + + + + + + + {{ $__t('Configure userfields') }} + + + {{ $userentity->name }} + + {{ $userentity->caption }} +
    +
    +
    +@stop diff --git a/views/userentityform.blade.php b/views/userentityform.blade.php new file mode 100644 index 00000000..345d8535 --- /dev/null +++ b/views/userentityform.blade.php @@ -0,0 +1,59 @@ +@extends('layout.default') + +@if($mode == 'edit') + @section('title', $__t('Edit userentity')) +@else + @section('title', $__t('Create userentity')) +@endif + +@section('viewJsName', 'userentityform') + +@section('content') +
    +
    +

    @yield('title')

    + + + + @if($mode == 'edit') + + @endif + +
    + +
    + + +
    {{ $__t('This is required and can only contain letters and numbers') }}
    +
    + +
    + + +
    {{ $__t('A caption is required') }}
    +
    + +
    + + +
    + +
    +
    + + show_in_sidebar_menu == 1) checked @endif class="form-check-input" type="checkbox" id="show_in_sidebar_menu" name="show_in_sidebar_menu" value="1"> + +
    +
    + +
    + + +
    + + + +
    +
    +
    +@stop diff --git a/views/userobjectform.blade.php b/views/userobjectform.blade.php new file mode 100644 index 00000000..a1a09dc4 --- /dev/null +++ b/views/userobjectform.blade.php @@ -0,0 +1,38 @@ +@extends('layout.default') + +@if($mode == 'edit') + @section('title', $__t('Edit %s', $userentity->caption)) +@else + @section('title', $__t('Create %s', $userentity->caption)) +@endif + +@section('viewJsName', 'userobjectform') + +@section('content') +
    +
    +

    @yield('title')

    + + + + @if($mode == 'edit') + + @endif + +
    + + @include('components.userfieldsform', array( + 'userfields' => $userfields, + 'entity' => 'userentity-' . $userentity->name + )) + + + +
    +
    +
    +@stop diff --git a/views/userobjects.blade.php b/views/userobjects.blade.php new file mode 100644 index 00000000..19e9f95a --- /dev/null +++ b/views/userobjects.blade.php @@ -0,0 +1,66 @@ +@extends('layout.default') + +@section('title', $userentity->caption) +@section('activeNav', 'userentity-' . $userentity->name) +@section('viewJsName', 'userobjects') + +@section('content') +
    +
    +

    + @yield('title') + +  {{ $__t('Add') }} + + +  {{ $__t('Configure userfields') }} + +

    +
    {{ $userentity->description }}
    +
    +
    + +
    +
    + + +
    +
    + +
    +
    + + + + + + @include('components.userfields_thead', array( + 'userfields' => $userfields + )) + + + + + @foreach($userobjects as $userobject) + + + + @include('components.userfields_tbody', array( + 'userfields' => $userfields, + 'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $userobject->id) + )) + + + @endforeach + +
    + + + + + + +
    +
    +
    +@stop