Feature/upload improvements (#617)

* various improvements.. needs cleanup

* more progress - supports deeply nested + pages

* Getting close now!

* more progress!

* some cleanup

* use data[_json] to store page-based upload

* Smarter logic to get nested form fields

* some refactoring/cleanup

* Fixed issue with removing multiple files in pages

* Refactor and support `destination: page@:/images` and `destination: self@` syntax for file fields

* Prettifying the upload field

* Handling Files API to better represent the selected files in the input field

* Better plurarl string

* Fixed harcoded height for input field

* revamped CSS!!!

* `fancy: false` turns off fancy styling

* Create folder if not exists

* Add support for @theme/theme@ destination

* Fixed create directory functionality to take into account resolved paths

* Don't allow @self on page to be uploaded to if not created

* added field languages

* css tweaks

* language integration
This commit is contained in:
Andy Miller
2016-05-26 14:49:45 -06:00
parent 429a00f439
commit 8cd0279b01
21 changed files with 617 additions and 266 deletions

View File

@@ -233,9 +233,11 @@ class Admin
* *
* @return Page * @return Page
*/ */
public function page($route = false) public function page($route = false, $path = null)
{ {
$path = $this->route; if (!$path) {
$path = $this->route;
}
if ($route && !$path) { if ($route && !$path) {
$path = '/'; $path = '/';
@@ -285,88 +287,57 @@ class Admin
$post = isset($_POST['data']) ? $_POST['data'] : []; $post = isset($_POST['data']) ? $_POST['data'] : [];
} }
switch ($type) { /** @var UniformResourceLocator $locator */
case 'configuration': $locator = $this->grav['locator'];
case 'system': $filename = $locator->findResource("config://{$type}.yaml", true, true);
$type = 'system'; $file = CompiledYamlFile::instance($filename);
$blueprints = $this->blueprints("config/{$type}");
$config = $this->grav['config'];
$obj = new Data\Data($config->get('system'), $blueprints);
$obj->merge($post);
$file = CompiledYamlFile::instance($this->grav['locator']->findResource("config://{$type}.yaml"));
$obj->file($file);
$data[$type] = $obj;
break;
case 'settings': if (preg_match('|plugins/|', $type)) {
case 'site': /** @var Plugins $plugins */
$type = 'site'; $plugins = $this->grav['plugins'];
$blueprints = $this->blueprints("config/{$type}"); $obj = $plugins->get(preg_replace('|plugins/|', '', $type));
$config = $this->grav['config'];
$obj = new Data\Data($config->get('site'), $blueprints);
$obj->merge($post);
$file = CompiledYamlFile::instance($this->grav['locator']->findResource("config://{$type}.yaml"));
$obj->file($file);
$data[$type] = $obj;
break;
case 'login': if (!$obj) { return []; }
$data[$type] = null;
break;
default: $obj->merge($post);
/** @var UniformResourceLocator $locator */ $obj->file($file);
$locator = $this->grav['locator'];
$filename = $locator->findResource("config://{$type}.yaml", true, true);
$file = CompiledYamlFile::instance($filename);
if (preg_match('|plugins/|', $type)) { $data[$type] = $obj;
/** @var Plugins $plugins */ } elseif (preg_match('|themes/|', $type)) {
$plugins = $this->grav['plugins']; /** @var Themes $themes */
$obj = $plugins->get(preg_replace('|plugins/|', '', $type)); $themes = $this->grav['themes'];
$obj = $themes->get(preg_replace('|themes/|', '', $type));
if (!$obj) { return []; } if (!$obj) { return []; }
$obj->merge($post); $obj->merge($post);
$obj->file($file); $obj->file($file);
$data[$type] = $obj; $data[$type] = $obj;
} elseif (preg_match('|themes/|', $type)) { } elseif (preg_match('|users/|', $type)) {
/** @var Themes $themes */ $obj = User::load(preg_replace('|users/|', '', $type));
$themes = $this->grav['themes']; $obj->merge($post);
$obj = $themes->get(preg_replace('|themes/|', '', $type));
if (!$obj) { return []; } $data[$type] = $obj;
} elseif (preg_match('|user/|', $type)) {
$obj = User::load(preg_replace('|user/|', '', $type));
$obj->merge($post);
$obj->merge($post); $data[$type] = $obj;
$obj->file($file); } elseif (preg_match('|config/|', $type)) {
$type = preg_replace('|config/|', '', $type);
$data[$type] = $obj; $blueprints = $this->blueprints("config/{$type}");
} elseif (preg_match('|users/|', $type)) { $config = $this->grav['config'];
$obj = User::load(preg_replace('|users/|', '', $type)); $obj = new Data\Data($config->get($type, []), $blueprints);
$obj->merge($post); $obj->merge($post);
// FIXME: We shouldn't allow user to change configuration files in system folder!
$data[$type] = $obj; $filename = $this->grav['locator']->findResource("config://{$type}.yaml")
} elseif (preg_match('|user/|', $type)) { ?: $this->grav['locator']->findResource("config://{$type}.yaml", true, true);
$obj = User::load(preg_replace('|user/|', '', $type)); $file = CompiledYamlFile::instance($filename);
$obj->merge($post); $obj->file($file);
$data[$type] = $obj;
$data[$type] = $obj; } else {
} elseif (preg_match('|config/|', $type)) { throw new \RuntimeException("Data type '{$type}' doesn't exist!");
$type = preg_replace('|config/|', '', $type);
$blueprints = $this->blueprints("config/{$type}");
$config = $this->grav['config'];
$obj = new Data\Data($config->get($type, []), $blueprints);
$obj->merge($post);
// FIXME: We shouldn't allow user to change configuration files in system folder!
$filename = $this->grav['locator']->findResource("config://{$type}.yaml")
?: $this->grav['locator']->findResource("config://{$type}.yaml", true, true);
$file = CompiledYamlFile::instance($filename);
$obj->file($file);
$data[$type] = $obj;
} else {
throw new \RuntimeException("Data type '{$type}' doesn't exist!");
}
} }
return $data[$type]; return $data[$type];
@@ -1180,4 +1151,63 @@ class Admin
{ {
$this->permissions = array_merge($this->permissions, $permissions); $this->permissions = array_merge($this->permissions, $permissions);
} }
public function findFormFields($type, $fields, $found_fields = [])
{
foreach ($fields as $key => $field) {
if (isset($field['type']) && $field['type'] == $type) {
$found_fields[$key] = $field;
} elseif (isset($field['fields'])) {
$result = $this->findFormFields($type, $field['fields'], $found_fields);
if (!empty($result)) {
$found_fields = array_merge($found_fields, $result);
}
}
}
return $found_fields;
}
public function getPagePathFromToken($path)
{
$path_parts = pathinfo($path);
$basename = '';
if (isset($path_parts['extension'])) {
$basename = '/'.$path_parts['basename'];
$path = $path_parts['dirname'];
}
$regex = '/(@self|self@)|((?:@page|page@):(?:.*))|((?:@theme|theme@):(?:.*))/';
preg_match($regex, $path, $matches);
if ($matches) {
if ($matches[1]) {
// self@
$page = $this->page(true);
} elseif ($matches[2]) {
// page@
$parts = explode(':', $path);
$route = $parts[1];
$page = $this->grav['page']->find($route);
} elseif ($matches[3]) {
// theme@
$parts = explode(':', $path);
$route = $parts[1];
$theme = str_replace(ROOT_DIR, '', $this->grav['locator']->findResource("theme://"));
return $theme . $route . $basename;
}
} else {
return $path . $basename;
}
if (!$page) {
throw new \RuntimeException('Page route not found: ' . $path);
}
$path = str_replace($matches[0], rtrim($page->relativePagePath(), '/'), $path);
return $path . $basename;
}
} }

View File

@@ -85,11 +85,11 @@ class AdminController
]; ];
/** /**
* @param Grav $grav * @param Grav $grav
* @param string $view * @param string $view
* @param string $task * @param string $task
* @param string $route * @param string $route
* @param array $post * @param array $post
*/ */
public function __construct(Grav $grav, $view, $task, $route, $post) public function __construct(Grav $grav, $view, $task, $route, $post)
{ {
@@ -123,15 +123,14 @@ class AdminController
$nonce = $this->grav['uri']->param('admin-nonce'); $nonce = $this->grav['uri']->param('admin-nonce');
} }
if (!$nonce || !Utils::verifyNonce($nonce, 'admin-form')) if (!$nonce || !Utils::verifyNonce($nonce, 'admin-form')) {
{
if ($this->task == 'addmedia') { if ($this->task == 'addmedia') {
$message = sprintf($this->admin->translate('PLUGIN_ADMIN.FILE_TOO_LARGE', null, true), ini_get('post_max_size')); $message = sprintf($this->admin->translate('PLUGIN_ADMIN.FILE_TOO_LARGE', null, true), ini_get('post_max_size'));
//In this case it's more likely that the image is too big than POST can handle. Show message //In this case it's more likely that the image is too big than POST can handle. Show message
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $message 'message' => $message
]; ];
@@ -140,7 +139,7 @@ class AdminController
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error'); $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN') 'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
]; ];
@@ -154,7 +153,7 @@ class AdminController
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'),
'error'); 'error');
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN') 'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
]; ];
@@ -166,7 +165,7 @@ class AdminController
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'),
'error'); 'error');
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN') 'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
]; ];
@@ -283,7 +282,7 @@ class AdminController
$language = $this->grav['user']->authenticated ? $this->grav['user']->language : ($this->grav['language']->getLanguage() ?: 'en'); $language = $this->grav['user']->authenticated ? $this->grav['user']->language : ($this->grav['language']->getLanguage() ?: 'en');
$this->admin->session()->invalidate()->start(); $this->admin->session()->invalidate()->start();
$this->setRedirect('/logout/lang:'.$language); $this->setRedirect('/logout/lang:' . $language);
return true; return true;
} }
@@ -464,7 +463,7 @@ class AdminController
} }
if ($result) { if ($result) {
$this->admin->json_response = ['status' => 'success', 'dependencies' => $dependencies, 'message' => $this->admin->translate(is_string($result) ? $result :'PLUGIN_ADMIN.UNINSTALL_SUCCESSFUL')]; $this->admin->json_response = ['status' => 'success', 'dependencies' => $dependencies, 'message' => $this->admin->translate(is_string($result) ? $result : 'PLUGIN_ADMIN.UNINSTALL_SUCCESSFUL')];
} else { } else {
$this->admin->json_response = ['status' => 'error', 'message' => $this->admin->translate('PLUGIN_ADMIN.UNINSTALL_FAILED')]; $this->admin->json_response = ['status' => 'error', 'message' => $this->admin->translate('PLUGIN_ADMIN.UNINSTALL_FAILED')];
} }
@@ -651,12 +650,12 @@ class AdminController
$results = Cache::clearCache($clear); $results = Cache::clearCache($clear);
if (count($results) > 0) { if (count($results) > 0) {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'success', 'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.CACHE_CLEARED') . ' <br />' . $this->admin->translate('PLUGIN_ADMIN.METHOD') . ': ' . $clear . '' 'message' => $this->admin->translate('PLUGIN_ADMIN.CACHE_CLEARED') . ' <br />' . $this->admin->translate('PLUGIN_ADMIN.METHOD') . ': ' . $clear . ''
]; ];
} else { } else {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.ERROR_CLEARING_CACHE') 'message' => $this->admin->translate('PLUGIN_ADMIN.ERROR_CLEARING_CACHE')
]; ];
} }
@@ -696,7 +695,7 @@ class AdminController
$backup = ZipBackup::backup(); $backup = ZipBackup::backup();
} catch (\Exception $e) { } catch (\Exception $e) {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.AN_ERROR_OCCURRED') . '. ' . $e->getMessage() 'message' => $this->admin->translate('PLUGIN_ADMIN.AN_ERROR_OCCURRED') . '. ' . $e->getMessage()
]; ];
@@ -708,18 +707,18 @@ class AdminController
'/') . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form'); '/') . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');
$log->content([ $log->content([
'time' => time(), 'time' => time(),
'location' => $backup 'location' => $backup
]); ]);
$log->save(); $log->save();
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'success', 'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.YOUR_BACKUP_IS_READY_FOR_DOWNLOAD') . '. <a href="' . $url . '" class="button">' . $this->admin->translate('PLUGIN_ADMIN.DOWNLOAD_BACKUP') . '</a>', 'message' => $this->admin->translate('PLUGIN_ADMIN.YOUR_BACKUP_IS_READY_FOR_DOWNLOAD') . '. <a href="' . $url . '" class="button">' . $this->admin->translate('PLUGIN_ADMIN.DOWNLOAD_BACKUP') . '</a>',
'toastr' => [ 'toastr' => [
'timeOut' => 0, 'timeOut' => 0,
'extendedTimeOut' => 0, 'extendedTimeOut' => 0,
'closeButton' => true 'closeButton' => true
] ]
]; ];
@@ -838,7 +837,7 @@ class AdminController
} }
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'success', 'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.PAGES_FILTERED'), 'message' => $this->admin->translate('PLUGIN_ADMIN.PAGES_FILTERED'),
'results' => $results 'results' => $results
]; ];
@@ -860,7 +859,7 @@ class AdminController
if (!$page) { if (!$page) {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.NO_PAGE_FOUND') 'message' => $this->admin->translate('PLUGIN_ADMIN.NO_PAGE_FOUND')
]; ];
@@ -894,7 +893,7 @@ class AdminController
if (!isset($_FILES['file']['error']) || is_array($_FILES['file']['error'])) { if (!isset($_FILES['file']['error']) || is_array($_FILES['file']['error'])) {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_PARAMETERS') 'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_PARAMETERS')
]; ];
@@ -907,7 +906,7 @@ class AdminController
break; break;
case UPLOAD_ERR_NO_FILE: case UPLOAD_ERR_NO_FILE:
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.NO_FILES_SENT') 'message' => $this->admin->translate('PLUGIN_ADMIN.NO_FILES_SENT')
]; ];
@@ -915,14 +914,14 @@ class AdminController
case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE: case UPLOAD_ERR_FORM_SIZE:
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT') 'message' => $this->admin->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT')
]; ];
return false; return false;
default: default:
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS') 'message' => $this->admin->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS')
]; ];
@@ -933,7 +932,7 @@ class AdminController
// You should also check filesize here. // You should also check filesize here.
if ($grav_limit > 0 && $_FILES['file']['size'] > $grav_limit) { if ($grav_limit > 0 && $_FILES['file']['size'] > $grav_limit) {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT') 'message' => $this->admin->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT')
]; ];
@@ -952,7 +951,7 @@ class AdminController
// If not a supported type, return // If not a supported type, return
if (!$fileExt || !$config->get("media.{$fileExt}")) { if (!$fileExt || !$config->get("media.{$fileExt}")) {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $fileExt 'message' => $this->admin->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $fileExt
]; ];
@@ -964,7 +963,7 @@ class AdminController
sprintf('%s/%s', $page->path(), $_FILES['file']['name'])) sprintf('%s/%s', $page->path(), $_FILES['file']['name']))
) { ) {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE') 'message' => $this->admin->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE')
]; ];
@@ -973,7 +972,7 @@ class AdminController
Cache::clearCache(); Cache::clearCache();
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'success', 'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY') 'message' => $this->admin->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY')
]; ];
@@ -995,7 +994,7 @@ class AdminController
if (!$page) { if (!$page) {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.NO_PAGE_FOUND') 'message' => $this->admin->translate('PLUGIN_ADMIN.NO_PAGE_FOUND')
]; ];
@@ -1010,12 +1009,12 @@ class AdminController
if (unlink($targetPath)) { if (unlink($targetPath)) {
Cache::clearCache(); Cache::clearCache();
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'success', 'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . $filename 'message' => $this->admin->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . $filename
]; ];
} else { } else {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename 'message' => $this->admin->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename
]; ];
} }
@@ -1043,12 +1042,12 @@ class AdminController
if ($deletedResponsiveImage) { if ($deletedResponsiveImage) {
Cache::clearCache(); Cache::clearCache();
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'success', 'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . $filename 'message' => $this->admin->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . $filename
]; ];
} else { } else {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.FILE_NOT_FOUND') . ': ' . $filename 'message' => $this->admin->translate('PLUGIN_ADMIN.FILE_NOT_FOUND') . ': ' . $filename
]; ];
} }
@@ -1056,7 +1055,7 @@ class AdminController
} }
} else { } else {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.NO_FILE_FOUND') 'message' => $this->admin->translate('PLUGIN_ADMIN.NO_FILE_FOUND')
]; ];
} }
@@ -1080,7 +1079,7 @@ class AdminController
if (!$page) { if (!$page) {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.NO_PAGE_FOUND') 'message' => $this->admin->translate('PLUGIN_ADMIN.NO_PAGE_FOUND')
]; ];
@@ -1216,15 +1215,15 @@ class AdminController
if ($result) { if ($result) {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'success', 'status' => 'success',
'type' => 'updategrav', 'type' => 'updategrav',
'version' => $version, 'version' => $version,
'message' => $this->admin->translate('PLUGIN_ADMIN.GRAV_WAS_SUCCESSFULLY_UPDATED_TO') . ' ' . $version 'message' => $this->admin->translate('PLUGIN_ADMIN.GRAV_WAS_SUCCESSFULLY_UPDATED_TO') . ' ' . $version
]; ];
} else { } else {
$this->admin->json_response = [ $this->admin->json_response = [
'status' => 'error', 'status' => 'error',
'type' => 'updategrav', 'type' => 'updategrav',
'version' => GRAV_VERSION, 'version' => GRAV_VERSION,
'message' => $this->admin->translate('PLUGIN_ADMIN.GRAV_UPDATE_FAILED') . ' <br>' . Installer::lastErrorMsg() 'message' => $this->admin->translate('PLUGIN_ADMIN.GRAV_UPDATE_FAILED') . ' <br>' . Installer::lastErrorMsg()
]; ];
@@ -1265,87 +1264,74 @@ class AdminController
} }
/** /**
* @param $field
* @return array * @return array
*/ */
private function cleanFilesData() private function cleanFilesData($field)
{ {
/** @var Page $page */ /** @var Page $page */
$page = null; $page = null;
$cleanFiles = []; $cleanFiles = [];
$type = trim("{$this->view}/{$this->admin->route}", '/');
$data = $this->admin->data($type, $this->post);
$blueprints = $data->blueprints();
if (!isset($blueprints['form']['fields'])) {
throw new \RuntimeException('Blueprints missing form fields definition');
}
$blueprint = $blueprints['form']['fields'];
$file = $_FILES['data']; $file = $_FILES['data'];
foreach ((array)$file['error'] as $index => $errors) { $errors = (array) Utils::getDotNotation($file['error'], $field['name']);
$errors = !is_array($errors) ? [$errors] : $errors;
foreach($errors as $multiple_index => $error) { foreach ($errors as $index => $error) {
if ($error == UPLOAD_ERR_OK) { if ($error == UPLOAD_ERR_OK) {
if (is_array($file['name'][$index])) {
$tmp_name = $file['tmp_name'][$index][$multiple_index];
$name = $file['name'][$index][$multiple_index];
$type = $file['type'][$index][$multiple_index];
$size = $file['size'][$index][$multiple_index];
} else {
$tmp_name = $file['tmp_name'][$index];
$name = $file['name'][$index];
$type = $file['type'][$index];
$size = $file['size'][$index];
}
$destination = Folder::getRelativePath(rtrim($blueprint[$index]['destination'], '/')); $fieldname = $field['name'];
if (!$this->match_in_array($type, $blueprint[$index]['accept'])) { // Deal with multiple files
throw new \RuntimeException('File "' . $name . '" is not an accepted MIME type.'); if (isset($field['multiple']) && $field['multiple'] == true) {
} $fieldname = $fieldname . ".$index";
}
if (Utils::startsWith($destination, '@page:')) { $tmp_name = Utils::getDotNotation($file['tmp_name'], $fieldname);
$parts = explode(':', $destination); $name = Utils::getDotNotation($file['name'], $fieldname);
$route = $parts[1]; $type = Utils::getDotNotation($file['type'], $fieldname);
$page = $this->grav['page']->find($route); $size = Utils::getDotNotation($file['size'], $fieldname);
if (!$page) { $original_destination = null ;
throw new \RuntimeException('Unable to upload file to destination. Page route not found.'); $destination = Folder::getRelativePath(rtrim($field['destination'], '/'));
}
$destination = $page->relativePagePath(); if (!$this->match_in_array($type, $field['accept'])) {
} else { throw new \RuntimeException('File "' . $name . '" is not an accepted MIME type.');
if ($destination == '@self') { }
$page = $this->admin->page(true);
$destination = $page->relativePagePath();
} else {
Folder::mkdir($destination);
}
}
if (move_uploaded_file($tmp_name, "$destination/$name")) { if (isset($field['random_name']) && $field['random_name'] === true) {
$path = $page ? $this->grav['uri']->convertUrl($page, $page->route() . '/' . $name) : $destination . '/' . $name; $path_parts = pathinfo($name);
$fileData = [ $name = Utils::generateRandomString(15) . '.' . $path_parts['extension'];
'name' => $name, }
'path' => $path,
'type' => $type,
'size' => $size,
'file' => $destination . '/' . $name,
'route' => $page ? $path : null
];
$cleanFiles[$index][$path] = $fileData; $resolved_destination = $this->admin->getPagePathFromToken($destination);
} else { $upload_path = $resolved_destination . '/' . $name;
throw new \RuntimeException("Unable to upload file(s) to $destination/$name");
} // Create dir if need be
if (!is_dir($resolved_destination)) {
Folder::mkdir($resolved_destination);
}
if (move_uploaded_file($tmp_name, $upload_path)) {
$path = $destination . '/' . $name;
$fileData = [
'name' => $name,
'path' => $path,
'type' => $type,
'size' => $size,
'file' => $destination . '/' . $name,
'route' => $page ? $path : null
];
$cleanFiles[$field['name']][$path] = $fileData;
} else { } else {
if ($error != UPLOAD_ERR_NO_FILE) { throw new \RuntimeException("Unable to upload file(s) to $destination/$name");
throw new \RuntimeException("Unable to upload file(s) - Error: ".$file['name'][$index].": ".$this->upload_errors[$error]); }
}
} else {
if ($error != UPLOAD_ERR_NO_FILE) {
throw new \RuntimeException("Unable to upload file(s) - Error: " . $field['name'] . ": " . $this->upload_errors[$error]);
} }
} }
} }
@@ -1354,7 +1340,7 @@ class AdminController
} }
/** /**
* @param string $needle * @param string $needle
* @param array|string $haystack * @param array|string $haystack
* *
* @return bool * @return bool
@@ -1382,16 +1368,60 @@ class AdminController
if (!isset($_FILES['data'])) { if (!isset($_FILES['data'])) {
return $obj; return $obj;
} }
$cleanFiles = $this->cleanFilesData();
foreach ($cleanFiles as $key => $data) { $blueprints = $obj->blueprints();
$obj->set($key, $data);
if (!isset($blueprints['form']['fields'])) {
throw new \RuntimeException('Blueprints missing form fields definition');
}
$fields = $blueprints['form']['fields'];
$found_files = $this->findFields('file', $fields);
foreach ($found_files as $key => $data) {
if ($this->view == 'pages') {
$keys = explode('.', preg_replace('/^header./', '', $key));
$init_key = array_shift($keys);
if (count($keys) > 0) {
$new_data = isset($obj->header()->$init_key) ? $obj->header()->$init_key: [];
Utils::setDotNotation($new_data, implode('.', $keys), $data);
} else {
$new_data = $data;
}
$obj->modifyHeader($init_key, $new_data);
} else {
$obj->set($key, $data);
}
} }
return $obj; return $obj;
} }
public function findFields($type, $fields, $found = [])
{
foreach ($fields as $key => $field) {
if (isset($field['type']) && $field['type'] == $type) {
$file_field = $this->cleanFilesData($field);
} elseif (isset($field['fields'])) {
$result = $this->findFields($type, $field['fields'], $found);
if (!empty($result)) {
$found = array_merge($found, $result);
}
} else {
$file_field = null;
}
if (isset($file_field) && (!is_array($file_field) || !empty($file_field))) {
$found = array_merge($file_field, $found);
}
}
return $found;
}
/** /**
* Handles creating an empty page folder (without markdown file) * Handles creating an empty page folder (without markdown file)
* *
@@ -1493,8 +1523,12 @@ class AdminController
// Find new parent page in order to build the path. // Find new parent page in order to build the path.
$route = !isset($data['route']) ? dirname($this->admin->route) : $data['route']; $route = !isset($data['route']) ? dirname($this->admin->route) : $data['route'];
/** @var Page $obj */
$obj = $this->admin->page(true); $obj = $this->admin->page(true);
// Ensure route is prefixed with a forward slash. // Ensure route is prefixed with a forward slash.
$route = '/' . ltrim($route, '/'); $route = '/' . ltrim($route, '/');
@@ -1530,6 +1564,8 @@ class AdminController
$obj = $obj->move($parent); $obj = $obj->move($parent);
$this->preparePage($obj, false, $obj->language()); $this->preparePage($obj, false, $obj->language());
$obj = $this->processFiles($obj);
// Reset slug and route. For now we do not support slug twig variable on save. // Reset slug and route. For now we do not support slug twig variable on save.
$obj->slug($original_slug); $obj->slug($original_slug);
@@ -1883,11 +1919,11 @@ class AdminController
/** /**
* Determine if the user can edit media * Determine if the user can edit media
* *
* @param string $type
* @return bool True if the media action is allowed * @return bool True if the media action is allowed
*/ */
protected function canEditMedia() protected function canEditMedia($type = 'media')
{ {
$type = 'media';
if (!$this->authorizeTask('edit media', ['admin.' . $type, 'admin.super'])) { if (!$this->authorizeTask('edit media', ['admin.' . $type, 'admin.super'])) {
return false; return false;
} }
@@ -1907,6 +1943,7 @@ class AdminController
} }
$filename = base64_decode($this->route); $filename = base64_decode($this->route);
$file = File::instance($filename); $file = File::instance($filename);
$resultRemoveMedia = false; $resultRemoveMedia = false;
$resultRemoveMediaMeta = true; $resultRemoveMediaMeta = true;
@@ -1943,31 +1980,58 @@ class AdminController
protected function taskRemoveFileFromBlueprint() protected function taskRemoveFileFromBlueprint()
{ {
$uri = $this->grav['uri']; $uri = $this->grav['uri'];
$this->taskRemoveMedia(); $blueprint = base64_decode($uri->param('blueprint'));
$path = base64_decode($uri->param('path'));
$proute = base64_decode($uri->param('proute'));
$type = $uri->param('type');
$field = $uri->param('field'); $field = $uri->param('field');
$blueprint = $uri->param('blueprint'); $this->taskRemoveMedia();
$path = base64_decode($uri->param('path')); if ($type == 'pages') {
$page = $this->admin->page(true, $proute);
$keys = explode('.', preg_replace('/^header./', '', $field));
$header = (array) $page->header();
$data_path = implode('.', $keys);
$data = Utils::getDotNotation($header, $data_path);
$files = $this->grav['config']->get($blueprint . '.' . $field); if (isset($data[$path])) {
unset($data[$path]);
Utils::setDotNotation($header, $data_path, $data);
$page->header($header);
}
foreach ($files as $key => $value) { $page->save();
if ($key == $path) { } else {
unset($files[$key]); $blueprint_prefix = $type == 'config' ? '': $type . '.';
$blueprint_name = str_replace('/blueprints', '', str_replace('config/', '', $blueprint));
$blueprint_field = $blueprint_prefix . $blueprint_name . '.' . $field;
$files = $this->grav['config']->get($blueprint_field);
foreach ($files as $key => $value) {
if ($key == $path) {
unset($files[$key]);
}
}
$this->grav['config']->set($blueprint_field, $files);
switch ($type) {
case 'config':
$data = $this->grav['config']->get($blueprint_name);
$config = $this->admin->data($blueprint, $data);
$config->save();
break;
case 'themes':
Theme::saveConfig($blueprint_name);
break;
case 'plugins':
Plugin::saveConfig($blueprint_name);
break;
} }
} }
//
$this->grav['config']->set($blueprint . '.' . $field, $files); //
if (substr($blueprint, 0, 7) == 'plugins') {
Plugin::saveConfig(substr($blueprint, 8));
}
if (substr($blueprint, 0, 6) == 'themes') {
Theme::saveConfig(substr($blueprint, 7));
}
$redirect = base64_decode($uri->param('redirect')); $redirect = base64_decode($uri->param('redirect'));
$route = $this->grav['config']->get('plugins.admin.route'); $route = $this->grav['config']->get('plugins.admin.route');

View File

@@ -528,8 +528,9 @@ PLUGIN_ADMIN:
ORDERING_DISABLED_BECAUSE_PARENT_SETTING_ORDER: "Parent setting order, ordering disabled" ORDERING_DISABLED_BECAUSE_PARENT_SETTING_ORDER: "Parent setting order, ordering disabled"
ORDERING_DISABLED_BECAUSE_PAGE_NOT_VISIBLE: "Page is not visible, ordering disabled" ORDERING_DISABLED_BECAUSE_PAGE_NOT_VISIBLE: "Page is not visible, ordering disabled"
ORDERING_DISABLED_BECAUSE_TOO_MANY_SIBLINGS: "Ordering via the admin is unsupported because there are more than 200 siblings" ORDERING_DISABLED_BECAUSE_TOO_MANY_SIBLINGS: "Ordering via the admin is unsupported because there are more than 200 siblings"
CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED: "You cannot add media files until you save the page. Just click 'Save' on top" CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED: "NOTE: You cannot add media files until you save the page. Just click 'Save' on top"
DROP_FILES_HERE_TO_UPLOAD: "Drop files here to upload" CANNOT_ADD_FILES_PAGE_NOT_SAVED: "NOTE: Page must be saved before you can upload files to it."
DROP_FILES_HERE_TO_UPLOAD: "Drop your files here or <strong>click in this area</strong>"
INSERT: "Insert" INSERT: "Insert"
UNDO: "Undo" UNDO: "Undo"
REDO: "Redo" REDO: "Redo"

View File

@@ -0,0 +1,22 @@
import $ from 'jquery';
import format from '../../utils/formatbytes';
$('body').on('change', '.form-input-file > input[type="file"]', (event) => {
let input = event.target;
let files = input.files;
let container = $(input).next();
if (files.length) {
let plural = files.length > 1 ? 's' : '';
let html = '';
html += `${files.length} file${plural} selected`;
html += '<ul>';
for (let i = 0; i < files.length; i++) {
html += `<li>${files[i].name} (${format(files[i].size, 2)})</li>`;
}
html += '</ul>';
container.html(html);
}
});

View File

@@ -4,6 +4,7 @@ import CollectionsField, { Instance as CollectionsFieldInstance } from './collec
import DateTimeField, { Instance as DateTimeFieldInstance } from './datetime'; import DateTimeField, { Instance as DateTimeFieldInstance } from './datetime';
import EditorField, { Instance as EditorFieldInstance } from './editor'; import EditorField, { Instance as EditorFieldInstance } from './editor';
import ColorpickerField, { Instance as ColorpickerFieldInstance } from './colorpicker'; import ColorpickerField, { Instance as ColorpickerFieldInstance } from './colorpicker';
import './files';
export default { export default {
SelectizeField: { SelectizeField: {

View File

@@ -1 +1,3 @@
@import url("//fonts.googleapis.com/css?family=Montserrat:400|Lato:300,400,700|Inconsolata:400,700");body,h5,h6,.badge,.note,.grav-mdeditor-preview,input,select,textarea,button,.selectize-input{font-family:"Lato","Helvetica","Tahoma","Geneva","Arial",sans-serif}h1,h2,h3,h4,#admin-menu li,.form-tabs>label,.label{font-family:"Montserrat","Helvetica","Tahoma","Geneva","Arial",sans-serif}code,kbd,pre,samp,body .CodeMirror{font-family:"Inconsolata","Monaco","Consolas","Lucida Console",monospace !important} @import url("//fonts.googleapis.com/css?family=Montserrat:400|Lato:300,400,700|Inconsolata:400,700");body,h5,h6,.badge,.note,.grav-mdeditor-preview,input,select,textarea,button,.selectize-input{font-family:"Lato","Helvetica","Tahoma","Geneva","Arial",sans-serif}h1,h2,h3,h4,#admin-menu li,.form-tabs>label,.label{font-family:"Montserrat","Helvetica","Tahoma","Geneva","Arial",sans-serif}code,kbd,pre,samp,body .CodeMirror{font-family:"Inconsolata","Monaco","Consolas","Lucida Console",monospace !important}
/*# sourceMappingURL=fonts.css.map */

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

View File

@@ -1 +1,3 @@
body,h5,h6,.badge,.note,.grav-mdeditor-preview,input,select,textarea,button,.selectize-input,h1,h2,h3,h4,#admin-menu li,.form-tabs>label,.label{font-family:"Helvetica Neue", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif}code,kbd,pre,samp,body .CodeMirror{font-family:"Monaco", "Consolas", "Lucida Console", monospace} body,h5,h6,.badge,.note,.grav-mdeditor-preview,input,select,textarea,button,.selectize-input,h1,h2,h3,h4,#admin-menu li,.form-tabs>label,.label{font-family:"Helvetica Neue", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif}code,kbd,pre,samp,body .CodeMirror{font-family:"Monaco", "Consolas", "Lucida Console", monospace}
/*# sourceMappingURL=simple-fonts.css.map */

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

View File

@@ -640,6 +640,34 @@ form {
} }
.form-input-file {
border: 4px dashed $form-border;
p {
color: $form-field-text;
}
ul {
color: $primary-accent-fg;
background: $primary-accent-bg;
}
}
.file-thumbnail-remove {
background: $button-bg;
.fa {
color: $button-text;
}
&:hover {
background: $button-text;
.fa {
color: $button-bg;
}
}
}
} }
.grav-editor-resizer { .grav-editor-resizer {

View File

@@ -29,16 +29,33 @@ form {
padding-left: 1rem; padding-left: 1rem;
} }
.thumbnail { .file-thumbnail-wrapper {
max-width: 200px;
vertical-align: top;
display: inline-block; display: inline-block;
position: relative;
} }
.thumbnail-remove { .file-thumbnail {
max-height: 150px;
vertical-align: top; vertical-align: top;
font-size: 1.4rem; display: inline-block;
line-height: 1; margin-bottom: 10px;
}
.file-thumbnail-remove {
position: absolute;
text-align: center;
display: block;
top: 3px;
right: 3px;
width: 25px;
height: 25px;
border-radius:100%;
.fa {
font-size: 20px;
line-height: 25px;
vertical-align: top;
}
} }
} }
@@ -113,6 +130,40 @@ form {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
.form-input-file {
position: relative;
min-height: 70px;
border-radius: $form-border-radius;
ul {
margin: 1rem 0;
text-align: left;
font-size: 1rem;
border-radius: $form-border-radius;
}
p {
display: block;
height: 100%;
text-align: center;
margin: 0;
padding: 0.8rem 1rem 0;
font-size: 1.2rem;
}
input {
position: absolute;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
outline: none;
opacity: 0;
}
}
.selectize-dropdown { .selectize-dropdown {
z-index: 100000; z-index: 100000;
} }

View File

@@ -7,42 +7,62 @@
{% set uri = global.grav.uri %} {% set uri = global.grav.uri %}
{% set files = global.files %} {% set files = global.files %}
{% set config = global.grav.config %} {% set config = global.grav.config %}
{% set blueprint = (global.plugin ? 'plugins.' : global.theme ? 'themes.' : '') ~ uri.basename %} {% set route = global.context.route() %}
<img class="thumbnail" src="{{ uri.rootUrl == '/' ? '/' : uri.rootUrl ~ '/'}}{{ path }}" alt="{{ path|replace({(files.destination ~ '/'): ''}) }}" /> {% set type = global.context.content() ? 'pages' : global.plugin ? 'plugins' : global.theme ? 'themes' : 'config' %}
<a class="thumbnail-remove" href="{{ uri.addNonce(global.base_url_relative ~ {% set blueprint_name = global.blueprints.getFilename %}
'/media/' ~ base64_encode(global.base_path ~ '/' ~ path) ~ {% if type == 'pages' %}
'/task' ~ config.system.param_sep ~ 'removeFileFromBlueprint' ~ {% set blueprint_name = type ~ '/' ~ blueprint_name %}
'/blueprint' ~ config.system.param_sep ~ blueprint ~ {% endif %}
'/field' ~ config.system.param_sep ~ files.name ~ {% set blueprint = base64_encode(blueprint_name) %}
'/path' ~ config.system.param_sep ~ base64_encode(value.path) ~ {% set real_path = global.admin.getPagePathFromToken(path) %}
'/redirect' ~ config.system.param_sep ~ base64_encode(uri.path), 'admin-form', 'admin-nonce') }}">
<i class="fa fa-close"></i> {% if type == 'pages' %}
</a> {% include 'forms/fields/hidden/hidden.html.twig' with {field: {name: 'data._json.' ~ global.field.name}, value:{(value.path):value}|raw|json_encode} %}
{% endif %}
<div class="file-thumbnail-wrapper">
<img class="file-thumbnail" src="{{ uri.rootUrl == '/' ? '/' : uri.rootUrl ~ '/'}}{{ real_path }}" alt="{{ path|replace({(files.destination ~ '/'): ''}) }}" />
<a class="file-thumbnail-remove" href="{{ uri.addNonce(global.base_url_relative ~
'/media/' ~ base64_encode(global.base_path ~ '/' ~ real_path) ~
'/task' ~ config.system.param_sep ~ 'removeFileFromBlueprint' ~
'/proute' ~ config.system.param_sep ~ base64_encode(route) ~
'/blueprint' ~ config.system.param_sep ~ blueprint ~
'/type' ~ config.system.param_sep ~ type ~
'/field' ~ config.system.param_sep ~ files.name ~
'/path' ~ config.system.param_sep ~ base64_encode(value.path) ~
'/redirect' ~ config.system.param_sep ~ base64_encode(uri.path), 'admin-form', 'admin-nonce') }}">
<i class="fa fa-fw fa-close"></i>
</a>
</div>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% block input %} {% block input %}
{% set page_can_upload = exists or (type == 'page' and not exists and not (field.destination starts with '@self' or field.destination starts with 'self@')) %}
{% if type is not defined or page_can_upload %}
{% for path, file in value %}
{{ _self.preview(path, file, _context) }}
{% endfor %}
<div class="form-input-wrapper {% if field.fancy is not same as(false) %}form-input-file{% endif %} {{ field.size|default('xlarge') }}">
<input
{# required attribute structures #}
name="{{ (scope ~ field.name)|fieldName ~ (files.multiple ? '[]' : '') }}"
{% block input_attributes %}
type="file"
{% if files.multiple %}multiple="multiple"{% endif %}
{% if files.accept %}accept="{{ files.accept|join(',') }}"{% endif %}
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
{% if field.random_name %}random="true"{% endif %}
{{ parent() }}
{% endblock %}
/>
{% if field.fancy is not same as(false) %}
<p>{{ "PLUGIN_ADMIN.DROP_FILES_HERE_TO_UPLOAD"|tu|raw }}</p>
{% endif %}
</div>
{% if not plugin and not theme %}
The "file" input field cannot be used in Pages Blueprints. It's intended to be used for Plugins and Themes blueprints.
Use the "pagemediaselect" type instead.
{% else %} {% else %}
{% for path, file in value %} <span class="note">{{ "PLUGIN_ADMIN.CANNOT_ADD_FILES_PAGE_NOT_SAVED"|tu|raw }}</span>
{{ _self.preview(path, file, _context) }}
{% endfor %}
<div class="form-input-wrapper {{ field.size }}">
<input
{# required attribute structures #}
name="{{ (scope ~ field.name)|fieldName ~ (files.multiple ? '[]' : '') }}"
{% block input_attributes %}
type="file"
{% if files.multiple %}multiple="multiple"{% endif %}
{% if files.accept %}accept="{{ files.accept|join(',') }}"{% endif %}
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
{{ parent() }}
{% endblock %}
/>
</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -14,9 +14,9 @@
<div class="form-tab"> <div class="form-tab">
<div class="form-field"> <div class="form-field">
<div class="form-label"> <div class="form-label">
<label> <span class="note">
{{ "PLUGIN_ADMIN.CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED"|tu }} {{ "PLUGIN_ADMIN.CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED"|tu }}
</label> </span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
{% set taxonomies = (taxonomies is null ? admin.data('site').taxonomies : taxonomies) %} {% set taxonomies = (taxonomies is null ? admin.data('config/site').taxonomies : taxonomies) %}
{% set parentname = field.name %} {% set parentname = field.name %}
{% for name in taxonomies %} {% for name in taxonomies %}

View File

@@ -25,12 +25,10 @@
{% endif %} {% endif %}
{% set modular = context.modular ? 'modular_' : '' %} {% set modular = context.modular ? 'modular_' : '' %}
{% set warn = config.plugins.admin.warnings.delete_page %} {% set warn = config.plugins.admin.warnings.delete_page %}
{% set admin_lang = admin.session.admin_lang ?: 'en' %} {% set admin_lang = admin.session.admin_lang ?: 'en' %}
{% set page_lang = context.language %} {% set page_lang = context.language %}
{% set type = 'page' %}
{% block stylesheets %} {% block stylesheets %}
{% if mode == 'edit' %} {% if mode == 'edit' %}

View File

@@ -1,18 +1,14 @@
{% set form_id = form_id ? form_id : 'blueprints' %} {% set form_id = form_id ? form_id : 'blueprints' %}
{% set scope = scope ?: 'data.' %} {% set scope = scope ?: 'data.' %}
{% set multipart = '' %} {% if admin.findFormFields('file', blueprints.fields) %}
{% for field in blueprints.fields %} {% set multipart = ' enctype="multipart/form-data"' %}
{% if field.type == 'file' %} {% endif %}
{% set multipart = ' enctype="multipart/form-data"' %}
{% endif %}
{% endfor %}
<form id="{{ form_id }}" method="post" data-grav-form="{{ form_id }}" data-grav-keepalive="true"{{ multipart|raw }}> <form id="{{ form_id }}" method="post" data-grav-form="{{ form_id }}" data-grav-keepalive="true"{{ multipart|raw }}>
{% for field in blueprints.fields %} {% for field in blueprints.fields %}
{% if field.type %} {% if field.type %}
{% set value = field.name ? data.value(field.name) : data.toArray %} {% set value = field.name ? data.value(field.name) : data.toArray %}
<div class="block block-{{ field.type }}"> <div class="block block-{{ field.type }}">
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %} {% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
</div> </div>