diff --git a/modules/backend/formwidgets/fileupload/assets/css/fileupload.css b/modules/backend/formwidgets/fileupload/assets/css/fileupload.css
index 2fc811c4e5..e9da8b4b11 100644
--- a/modules/backend/formwidgets/fileupload/assets/css/fileupload.css
+++ b/modules/backend/formwidgets/fileupload/assets/css/fileupload.css
@@ -65,7 +65,7 @@
.field-fileupload.style-image-multi .upload-object .progress-bar .upload-progress{float:left;width:0%;height:100%;line-height:5px;color:#fff;background-color:#5fb6f5;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:width 0.6s ease;transition:width 0.6s ease}
.field-fileupload.style-image-multi .upload-object .icon-container{border-right:1px solid #f6f8f9;float:left;display:inline-block;overflow:hidden;width:75px;height:75px}
.field-fileupload.style-image-multi .upload-object .icon-container i{font-size:35px}
-.field-fileupload.style-image-multi .upload-object .icon-container.image img{border-bottom-left-radius:3px;border-top-left-radius:3px;width:auto}
+.field-fileupload.style-image-multi .upload-object .icon-container.image img{border-bottom-left-radius:3px;border-top-left-radius:3px;width:100%;height:100%;object-fit:cover}
.field-fileupload.style-image-multi .upload-object .info{margin-left:90px}
.field-fileupload.style-image-multi .upload-object .info h4{padding-right:15px}
.field-fileupload.style-image-multi .upload-object .info h4 a{right:15px}
@@ -154,4 +154,4 @@
.field-fileupload.style-file-single .upload-object .meta{position:absolute;top:50%;margin-top:-44px;height:88px;right:0;width:15%}
.field-fileupload.style-file-single .upload-object .meta .upload-remove-button{position:absolute;top:50%;right:0;height:20px;margin-top:-10px;margin-right:10px;z-index:100}
.field-fileupload.style-file-single .upload-object .icon-container:after{width:20px;height:20px;margin-top:-10px;margin-left:-10px;background-size:20px 20px}
-.field-fileupload.style-file-single .upload-object.is-error .icon-container:after{font-size:20px}
\ No newline at end of file
+.field-fileupload.style-file-single .upload-object.is-error .icon-container:after{font-size:20px}
diff --git a/modules/backend/formwidgets/fileupload/assets/less/fileupload.imagemulti.less b/modules/backend/formwidgets/fileupload/assets/less/fileupload.imagemulti.less
index 79c2eb32e7..734b8b87d1 100644
--- a/modules/backend/formwidgets/fileupload/assets/less/fileupload.imagemulti.less
+++ b/modules/backend/formwidgets/fileupload/assets/less/fileupload.imagemulti.less
@@ -45,7 +45,9 @@
&.image img {
.border-left-radius(3px);
- width: auto;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
}
}
@@ -131,4 +133,4 @@
width: auto;
}
}
-}
\ No newline at end of file
+}
diff --git a/modules/backend/formwidgets/repeater/assets/css/repeater.css b/modules/backend/formwidgets/repeater/assets/css/repeater.css
index 7de5800bd2..fddab40d60 100644
--- a/modules/backend/formwidgets/repeater/assets/css/repeater.css
+++ b/modules/backend/formwidgets/repeater/assets/css/repeater.css
@@ -52,11 +52,11 @@
.field-repeater .field-repeater-add-item:active{background:#3498db;border-color:transparent}
.field-repeater .field-repeater-add-item:active>a{color:#fff}
.field-repeater .field-repeater-add-item.in-progress{border-color:#e0e0e0 !important;background:transparent !important}
-.field-repeater[data-mode="grid"] ul.field-repeater-items{display:grid;gap:20px}
-.field-repeater[data-mode="grid"] ul.field-repeater-items .field-repeater-item{margin-bottom:0 !important}
-.field-repeater[data-mode="grid"] ul.field-repeater-items .field-repeater-add-item{margin-top:0}
-.field-repeater[data-mode="grid"] ul.field-repeater-items .field-repeater-add-item a{display:flex;flex-direction:column;justify-content:center;height:100%}
-.field-repeater[data-mode="grid"] ul.field-repeater-items .field-repeater-add-item:before{display:none}
+.field-repeater[data-mode="grid"]>ul.field-repeater-items{display:grid;gap:20px}
+.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-item{margin-bottom:0 !important}
+.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item{margin-top:0}
+.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item>a{display:flex;flex-direction:column;justify-content:center;height:100%}
+.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item:before{display:none}
.field-repeater[data-mode="grid"][data-columns="2"] ul.field-repeater-items{grid-template-columns:repeat(2,1fr)}
.field-repeater[data-mode="grid"][data-columns="3"] ul.field-repeater-items{grid-template-columns:repeat(3,1fr)}
.field-repeater[data-mode="grid"][data-columns="4"] ul.field-repeater-items{grid-template-columns:repeat(4,1fr)}
@@ -65,5 +65,5 @@
@media (max-width:1600px){.field-repeater[data-mode="grid"][data-columns="5"] ul.field-repeater-items{grid-template-columns:repeat(4,1fr)}}
.field-repeater[data-mode="grid"][data-columns="6"] ul.field-repeater-items{grid-template-columns:repeat(6,1fr)}
@media (max-width:1600px){.field-repeater[data-mode="grid"][data-columns="6"] ul.field-repeater-items{grid-template-columns:repeat(4,1fr)}}
-@media (min-width:768px) and (max-width:1199px){.field-repeater[data-mode="grid"] ul.field-repeater-items{grid-template-columns:repeat(2,1fr) !important}}
-@media (max-width:767px){.field-repeater[data-mode="grid"] ul.field-repeater-items{grid-template-columns:1fr !important}.field-repeater[data-mode="grid"] ul.field-repeater-items .field-repeater-item,.field-repeater[data-mode="grid"] ul.field-repeater-items .field-repeater-add-item{min-height:0 !important}.field-repeater[data-mode="grid"] ul.field-repeater-items .field-repeater-add-item{margin-top:10px}.field-repeater[data-mode="grid"] ul.field-repeater-items .field-repeater-add-item::before{display:block}}
\ No newline at end of file
+@media (min-width:768px) and (max-width:1199px){.field-repeater[data-mode="grid"]>ul.field-repeater-items{grid-template-columns:repeat(2,1fr) !important}}
+@media (max-width:767px){.field-repeater[data-mode="grid"]>ul.field-repeater-items{grid-template-columns:1fr !important}.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-item,.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item{min-height:0 !important}.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item{margin-top:10px}.field-repeater[data-mode="grid"]>ul.field-repeater-items>.field-repeater-add-item::before{display:block}}
\ No newline at end of file
diff --git a/modules/backend/formwidgets/repeater/assets/less/repeater.less b/modules/backend/formwidgets/repeater/assets/less/repeater.less
index e390e3ba53..ec20f3c438 100644
--- a/modules/backend/formwidgets/repeater/assets/less/repeater.less
+++ b/modules/backend/formwidgets/repeater/assets/less/repeater.less
@@ -243,18 +243,18 @@
}
&[data-mode="grid"] {
- ul.field-repeater-items {
+ > ul.field-repeater-items {
display: grid;
gap: 20px;
- .field-repeater-item {
+ > .field-repeater-item {
margin-bottom: 0 !important;
}
- .field-repeater-add-item {
+ > .field-repeater-add-item {
margin-top: 0;
- a {
+ > a {
display: flex;
flex-direction: column;
justify-content: center;
@@ -296,21 +296,21 @@
}
@media (min-width: @screen-sm-min) and (max-width: @screen-md-max) {
- ul.field-repeater-items {
+ > ul.field-repeater-items {
grid-template-columns: repeat(2, 1fr) !important;;
}
}
@media (max-width: @screen-xs-max) {
- ul.field-repeater-items {
+ > ul.field-repeater-items {
grid-template-columns: 1fr !important;
- .field-repeater-item,
- .field-repeater-add-item {
+ > .field-repeater-item,
+ > .field-repeater-add-item {
min-height: 0 !important;
}
- .field-repeater-add-item {
+ > .field-repeater-add-item {
margin-top: 10px;
&::before {
diff --git a/modules/backend/formwidgets/richeditor/assets/js/build-plugins-min.js b/modules/backend/formwidgets/richeditor/assets/js/build-plugins-min.js
index 85556851b3..eb0ac276d4 100644
--- a/modules/backend/formwidgets/richeditor/assets/js/build-plugins-min.js
+++ b/modules/backend/formwidgets/richeditor/assets/js/build-plugins-min.js
@@ -145,7 +145,7 @@ this.$textarea.on('froalaEditor.paste.beforeCleanup',this.proxy(this.beforeClean
this.$form.on('oc.beforeRequest',this.proxy(this.onFormBeforeRequest))
this.$textarea.froalaEditor(froalaOptions)
this.editor=this.$textarea.data('froala.editor')
-if(this.options.readOnly){this.editor.edit.off()}this.$el.on('keydown','.fr-view figure',this.proxy(this.onFigureKeydown))
+this.editor.$box.on('change',function(e){e.stopPropagation()});if(this.options.readOnly){this.editor.edit.off()}this.$el.on('keydown','.fr-view figure',this.proxy(this.onFigureKeydown))
Snowboard.globalEvent("formwidgets.richeditor.init",this)}
RichEditor.prototype.dispose=function(){this.unregisterHandlers()
this.$textarea.froalaEditor('destroy')
diff --git a/modules/backend/formwidgets/richeditor/assets/js/richeditor.js b/modules/backend/formwidgets/richeditor/assets/js/richeditor.js
index e1ef2d77c7..60aaff1755 100755
--- a/modules/backend/formwidgets/richeditor/assets/js/richeditor.js
+++ b/modules/backend/formwidgets/richeditor/assets/js/richeditor.js
@@ -233,6 +233,12 @@
this.editor = this.$textarea.data('froala.editor')
+ // Stop unnecessary "change" events from making it to the field element;
+ // only the textarea should be able to trigger the change event.
+ this.editor.$box.on('change', function (e) {
+ e.stopPropagation()
+ });
+
if (this.options.readOnly) {
this.editor.edit.off()
}
diff --git a/modules/backend/lang/en/lang.php b/modules/backend/lang/en/lang.php
index a23509f1d1..6d57657dbf 100644
--- a/modules/backend/lang/en/lang.php
+++ b/modules/backend/lang/en/lang.php
@@ -327,6 +327,7 @@
'relationwidget_unsupported_type' => 'The ":type" relation type is unsupported by the Relation widget.',
'help' => 'Click on an item to add',
'related_data' => 'Related :name data',
+ 'refresh' => 'Refresh',
'add' => 'Add',
'add_selected' => 'Add selected',
'add_a_new' => 'Add a new :name',
diff --git a/modules/backend/lang/lv/lang.php b/modules/backend/lang/lv/lang.php
index e751c10d46..a005939e6c 100644
--- a/modules/backend/lang/lv/lang.php
+++ b/modules/backend/lang/lv/lang.php
@@ -327,6 +327,7 @@
'relationwidget_unsupported_type' => 'Relāciju logrīks neatbalsta relāciju veidu ":type".',
'help' => 'Noklikšķiniet uz vienuma, lai pievienotu',
'related_data' => 'Saistītie :name dati',
+ 'refresh' => 'Atsvaidzināt',
'add' => 'Pievienot',
'add_selected' => 'Pievienot izvēlētos',
'add_a_new' => 'Pievienot jaunu :name',
diff --git a/modules/backend/layouts/_head.php b/modules/backend/layouts/_head.php
index bcaaa29154..f1b4526c21 100644
--- a/modules/backend/layouts/_head.php
+++ b/modules/backend/layouts/_head.php
@@ -29,7 +29,7 @@
Url::asset('modules/system/assets/js/build/manifest.js'),
Url::asset('modules/system/assets/js/snowboard/build/snowboard.vendor.js'),
Url::asset(
- (Config::get('develop.debugSnowboard', Config::get('app.debug', false)) === true)
+ (Config::get('develop.debugSnowboard', false) === true)
? 'modules/system/assets/js/build/system.debug.js'
: 'modules/system/assets/js/build/system.js'
),
diff --git a/modules/backend/widgets/Form.php b/modules/backend/widgets/Form.php
index d182570a63..6aed0c2834 100644
--- a/modules/backend/widgets/Form.php
+++ b/modules/backend/widgets/Form.php
@@ -1177,6 +1177,7 @@ protected function showFieldLabels($field)
public function getSaveData()
{
$this->defineFormFields();
+ $this->applyFiltersFromModel();
$result = [];
@@ -1342,6 +1343,11 @@ public function getOptionsFromModel($field, $fieldOptions)
]));
}
return $result;
+ } else {
+ // Handle localization keys that return arrays
+ if (is_array($options = Lang::get($fieldOptions))) {
+ return $options;
+ }
}
}
diff --git a/modules/backend/widgets/Lists.php b/modules/backend/widgets/Lists.php
index 0b78b483c0..65b8d742ce 100644
--- a/modules/backend/widgets/Lists.php
+++ b/modules/backend/widgets/Lists.php
@@ -510,7 +510,9 @@ public function prepareQuery()
$relationObj = $this->model->{$column->relation}();
$countQuery = $relationObj->getRelationExistenceQuery($relationObj->getRelated()->newQueryWithoutScopes(), $query);
- $joinSql = $this->isColumnRelated($column, true)
+ $limit = $column->config['limit'] ?? false;
+
+ $joinSql = $this->isColumnRelated($column, true) && $limit !== 1
? DbDongle::raw("group_concat(" . $sqlSelect . " separator ', ')")
: DbDongle::raw($sqlSelect);
@@ -520,6 +522,10 @@ public function prepareQuery()
$joinQuery->whereRaw(DbDongle::parse($column->config['conditions']));
}
+ if ($limit) {
+ $joinQuery->limit($column->config['limit']);
+ }
+
$joinSql = $joinQuery->toSql();
$selects[] = Db::raw("(".$joinSql.") as ".$alias);
@@ -1053,6 +1059,10 @@ public function getColumnValueRaw($record, $column)
}
}
+ if ($value instanceof \BackedEnum) {
+ $value = $value->value;
+ }
+
/**
* @event backend.list.overrideColumnValueRaw
* Overrides the raw column value in a list widget.
diff --git a/modules/backend/widgets/MediaManager.php b/modules/backend/widgets/MediaManager.php
index 8bb5b6fee0..067d53a50d 100644
--- a/modules/backend/widgets/MediaManager.php
+++ b/modules/backend/widgets/MediaManager.php
@@ -21,6 +21,7 @@
class MediaManager extends WidgetBase
{
use \Backend\Traits\UploadableWidget;
+ use \Backend\Traits\PreferenceMaker;
const FOLDER_ROOT = '/';
@@ -86,9 +87,8 @@ protected function loadAssets()
/**
* Abort the request with an access-denied code if readOnly mode is active
- * @return void
*/
- protected function abortIfReadOnly()
+ protected function abortIfReadOnly(): void
{
if ($this->readOnly) {
abort(403);
@@ -97,9 +97,8 @@ protected function abortIfReadOnly()
/**
* Renders the widget.
- * @return string
*/
- public function render()
+ public function render(): string
{
$this->prepareVars();
@@ -111,7 +110,7 @@ public function render()
//
/**
- * Perform search AJAX handler
+ * Perform a search with the query specified in the request ("search")
*/
public function onSearch(): array
{
@@ -126,7 +125,7 @@ public function onSearch(): array
}
/**
- * Change view AJAX handler
+ * Go to the path specified in the request ("path")
*/
public function onGoToFolder(): array
{
@@ -150,7 +149,7 @@ public function onGoToFolder(): array
}
/**
- * Generate thumbnail AJAX handler
+ * Generate thumbnails for the provided array of thumbnail info ("batch")
*/
public function onGenerateThumbnails(): array
{
@@ -170,7 +169,7 @@ public function onGenerateThumbnails(): array
}
/**
- * Get thumbnail AJAX handler
+ * Get the thumbnail for the provided path ("path") and lastModified date ("lastModified")
*
* @throws ApplicationException if the lastModified date is invalid
*/
@@ -197,7 +196,7 @@ public function onGetSidebarThumbnail(): array
}
/**
- * Set view preference AJAX handler
+ * Render the view for the provided "path" and "view" mode from the request
*/
public function onChangeView(): array
{
@@ -217,7 +216,7 @@ public function onChangeView(): array
}
/**
- * Set filter preference AJAX handler
+ * Set the current filter from the request ("filter")
*/
public function onSetFilter(): array
{
@@ -237,7 +236,7 @@ public function onSetFilter(): array
}
/**
- * Set sorting preference AJAX handler
+ * Set the current sorting configuration from the request ("sortBy", "sortDirection")
*/
public function onSetSorting(): array
{
@@ -258,7 +257,7 @@ public function onSetSorting(): array
}
/**
- * Delete library item AJAX handler
+ * Deletes the provided paths from the request ("paths")
*
* @throws ApplicationException if the paths input is invalid
* @todo Move media events to the MediaLibary class instead.
@@ -356,7 +355,7 @@ public function onDeleteItem(): array
}
/**
- * Show rename item popup AJAX handler
+ * Render the rename popup for the provided "path" from the request
*/
public function onLoadRenamePopup(): string
{
@@ -374,7 +373,7 @@ public function onLoadRenamePopup(): string
}
/**
- * Rename library item AJAX handler
+ * Rename the provided path from the request ("originalPath") to the new name ("name")
*
* @throws ApplicationException if the new name is invalid
* @todo Move media events to the MediaLibary class instead.
@@ -458,7 +457,7 @@ public function onApplyName(): void
}
/**
- * Create library folder AJAX handler
+ * Create a new folder ("name") in the provided "path" from the request
*
* @throws ApplicationException If the requested folder already exists or is otherwise invalid
*/
@@ -522,7 +521,7 @@ public function onCreateFolder(): array
}
/**
- * Show move item popup AJAX handler
+ * Render the move popup with a list of folders to move the selected items to excluding the provided paths in the request ("exclude")
*
* @throws ApplicationException If the exclude input data is not an array
*/
@@ -558,7 +557,7 @@ public function onLoadMovePopup(): string
}
/**
- * Move library item AJAX handler
+ * Move the selected items ("files", "folders") to the provided destination path from the request ("dest")
*
* @throws ApplicationException if the input data is invalid
*/
@@ -650,7 +649,7 @@ public function onMoveItems(): array
}
/**
- * Sidebar visibility AJAX handler
+ * Sets the sidebar visibility state from the request ("visible")
*/
public function onSetSidebarVisible(): void
{
@@ -660,7 +659,7 @@ public function onSetSidebarVisible(): void
}
/**
- * Renders the widget in a popup body
+ * Renders the widget in a popup body (options include "bottomToolbar" and "cropAndInsertButton")
*/
public function onLoadPopup(): string
{
@@ -905,11 +904,9 @@ protected function findFiles($searchTerm, $filter, $sortBy)
}
/**
- * Sets the user current folder from the session state
- *
- * @param string $path
+ * Sets the provided path as the current folder in the session
*/
- protected function setCurrentFolder($path): void
+ protected function setCurrentFolder(string $path): void
{
$path = MediaLibrary::validatePath($path);
@@ -917,21 +914,17 @@ protected function setCurrentFolder($path): void
}
/**
- * Gets the user current folder from the session state
- *
- * @return string
+ * Gets the user's current folder from the session
*/
- protected function getCurrentFolder()
+ protected function getCurrentFolder(): string
{
return $this->getSession('media_folder', self::FOLDER_ROOT);
}
/**
- * Sets the user filter from the session state
- *
- * @param string $filter
+ * Sets the user filter from the session
*/
- protected function setFilter($filter): void
+ protected function setFilter(string $filter): void
{
if (!in_array($filter, [
self::FILTER_ALL,
@@ -984,20 +977,16 @@ protected function setSearchTerm($searchTerm): void
/**
* Gets the user search term from the session state
- *
- * @return string
*/
- protected function getSearchTerm()
+ protected function getSearchTerm(): ?string
{
return $this->getSession('media_search', null);
}
/**
- * Sets the user sort column from the session state
- *
- * @param string $sortBy
+ * Sets the sort column
*/
- protected function setSortBy($sortBy): void
+ protected function setSortBy(string $sortBy): void
{
if (!in_array($sortBy, [
MediaLibrary::SORT_BY_TITLE,
@@ -1007,21 +996,22 @@ protected function setSortBy($sortBy): void
throw new ApplicationException('Invalid input data');
}
- $this->putSession('media_sort_by', $sortBy);
+ $key = 'media_sort_by';
+ $this->putUserPreference($key, $sortBy);
+ $this->putSession($key, $sortBy);
}
/**
- * Gets the user sort column from the session state
- *
- * @return string
+ * Gets the current column to sort by
*/
- protected function getSortBy()
+ protected function getSortBy(): string
{
- return $this->getSession('media_sort_by', MediaLibrary::SORT_BY_TITLE);
+ $key = 'media_sort_by';
+ return $this->getSession($key, $this->getUserPreference($key, MediaLibrary::SORT_BY_TITLE));
}
/**
- * Sets the user sort direction from the session state
+ * Sets the sort direction from the session state
*
* @param string $sortDirection
*/
@@ -1034,17 +1024,18 @@ protected function setSortDirection($sortDirection): void
throw new ApplicationException('Invalid input data');
}
- $this->putSession('media_sort_direction', $sortDirection);
+ $key = 'media_sort_direction';
+ $this->putUserPreference($key, $sortDirection);
+ $this->putSession($key, $sortDirection);
}
/**
* Gets the user sort direction from the session state
- *
- * @return string
*/
- protected function getSortDirection()
+ protected function getSortDirection(): string
{
- return $this->getSession('media_sort_direction', MediaLibrary::SORT_DIRECTION_ASC);
+ $key = 'media_sort_direction';
+ return $this->getSession($key, $this->getUserPreference($key, MediaLibrary::SORT_DIRECTION_ASC));
}
/**
@@ -1184,7 +1175,9 @@ protected function setViewMode(string $viewMode): void
throw new ApplicationException('Invalid input data');
}
- $this->putSession('view_mode', $viewMode);
+ $key = 'view_mode';
+ $this->putUserPreference($key, $viewMode);
+ $this->putSession($key, $viewMode);
}
/**
@@ -1192,7 +1185,8 @@ protected function setViewMode(string $viewMode): void
*/
protected function getViewMode(): string
{
- return $this->getSession('view_mode', self::VIEW_MODE_GRID);
+ $key = 'view_mode';
+ return $this->getSession($key, $this->getUserPreference($key, self::VIEW_MODE_GRID));
}
/**
@@ -1371,4 +1365,15 @@ protected function isVector(string $path): bool
{
return (pathinfo($path, PATHINFO_EXTENSION) == 'svg');
}
+
+ /**
+ * Returns a unique identifier for this widget and controller action for preference storage.
+ *
+ * @return string
+ */
+ protected function getPreferenceKey()
+ {
+ // User preferences should persist across controller usages for the MediaManager
+ return "backend::widgets.media_manager." . strtolower($this->getId());
+ }
}
diff --git a/modules/backend/widgets/toolbar/partials/_toolbar.php b/modules/backend/widgets/toolbar/partials/_toolbar.php
index bf43b9d710..662a478820 100644
--- a/modules/backend/widgets/toolbar/partials/_toolbar.php
+++ b/modules/backend/widgets/toolbar/partials/_toolbar.php
@@ -1,17 +1,21 @@
+
\ No newline at end of file
+
+
diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php
index c554b2f9ee..f68fb1861c 100644
--- a/modules/cms/classes/AutoDatasource.php
+++ b/modules/cms/classes/AutoDatasource.php
@@ -1,13 +1,15 @@
-postProcessor = new Processor;
}
+ /**
+ * Append a datasource to the end of the list of datasources
+ */
+ public function appendDatasource(string $key, DatasourceInterface $datasource): void
+ {
+ $this->datasources[$key] = $datasource;
+ $this->pathCache[] = Cache::rememberForever($datasource->getPathsCacheKey(), function () use ($datasource) {
+ return $datasource->getAvailablePaths();
+ });
+ }
+
+ /**
+ * Prepend a datasource to the beginning of the list of datasources
+ */
+ public function prependDatasource(string $key, DatasourceInterface $datasource): void
+ {
+ $this->datasources = array_prepend($this->datasources, $datasource, $key);
+ $this->pathCache = array_prepend($this->pathCache, Cache::rememberForever($datasource->getPathsCacheKey(), function () use ($datasource) {
+ return $datasource->getAvailablePaths();
+ }), $key);
+ }
+
/**
* Returns the in memory path cache map
*/
@@ -80,9 +104,8 @@ public function getPathCache(): array
* Populate the local cache of paths available in each datasource
*
* @param boolean $refresh Default false, set to true to force the cache to be rebuilt
- * @return void
*/
- public function populateCache($refresh = false)
+ public function populateCache(bool $refresh = false): void
{
$pathCache = [];
foreach ($this->datasources as $datasource) {
@@ -108,12 +131,8 @@ public function populateCache($refresh = false)
/**
* Check to see if the specified datasource has the provided Halcyon Model
- *
- * @param string $source The string key of the datasource to check
- * @param Model $model The Halcyon Model to check for
- * @return boolean
*/
- public function sourceHasModel(string $source, Model $model)
+ public function sourceHasModel(string $source, Model $model): bool
{
if (!$model->exists) {
return false;
@@ -141,11 +160,8 @@ public function sourceHasModel(string $source, Model $model)
/**
* Get the available paths for the specified datasource key
- *
- * @param string $source The string key of the datasource to check
- * @return void
*/
- public function getSourcePaths(string $source)
+ public function getSourcePaths(string $source): array
{
$result = [];
@@ -164,11 +180,9 @@ public function getSourcePaths(string $source)
/**
* Forces all operations in a provided closure to run within a selected datasource.
*
- * @param string $source
- * @param \Closure $closure
- * @return mixed
+ * @throws ApplicationException if the provided datasource key doesn't exist
*/
- public function usingSource(string $source, \Closure $closure)
+ public function usingSource(string $source, \Closure $closure): mixed
{
if (!array_key_exists($source, $this->datasources)) {
throw new ApplicationException('Invalid datasource specified.');
@@ -191,12 +205,8 @@ public function usingSource(string $source, \Closure $closure)
/**
* Push the provided model to the specified datasource
- *
- * @param Model $model The Halcyon Model to push
- * @param string $source The string key of the datasource to use
- * @return void
*/
- public function pushToSource(Model $model, string $source)
+ public function pushToSource(Model $model, string $source): void
{
$this->usingSource($source, function () use ($model) {
$datasource = $this->getActiveDatasource();
@@ -215,12 +225,8 @@ public function pushToSource(Model $model, string $source)
/**
* Remove the provided model from the specified datasource
- *
- * @param Model $model The Halcyon model to remove
- * @param string $source The string key of the datasource to use
- * @return void
*/
- public function removeFromSource(Model $model, string $source)
+ public function removeFromSource(Model $model, string $source): void
{
$this->usingSource($source, function () use ($model) {
$datasource = $this->getActiveDatasource();
@@ -236,11 +242,8 @@ public function removeFromSource(Model $model, string $source)
/**
* Get the appropriate datasource for the provided path
- *
- * @param string $path
- * @return Datasource
*/
- protected function getDatasourceForPath(string $path)
+ protected function getDatasourceForPath(string $path): DatasourceInterface
{
// Always return the active datasource when singleDatasourceMode is enabled
if ($this->singleDatasourceMode) {
@@ -283,7 +286,7 @@ protected function getDatasourceForPath(string $path)
* ];
* @return array $paths ["$dirName/path/1.md", "$dirName/path/2.md"]
*/
- protected function getValidPaths(string $dirName, array $options = [])
+ protected function getValidPaths(string $dirName, array $options = []): array
{
// Initialize result set
$paths = [];
@@ -325,23 +328,16 @@ protected function getValidPaths(string $dirName, array $options = [])
/**
* Helper to make file path.
- *
- * @param string $dirName
- * @param string $fileName
- * @param string $extension
- * @return string
*/
- protected function makeFilePath(string $dirName, string $fileName, string $extension)
+ protected function makeFilePath(string $dirName, string $fileName, string $extension): string
{
return ltrim($dirName . '/' . $fileName . '.' . $extension, '/');
}
/**
* Get the datasource for use with CRUD operations
- *
- * @return DatasourceInterface
*/
- protected function getActiveDatasource()
+ protected function getActiveDatasource(): DatasourceInterface
{
return $this->datasources[$this->activeDatasourceKey];
}
diff --git a/modules/cms/classes/CmsObject.php b/modules/cms/classes/CmsObject.php
index c2600a505a..e1ce327278 100644
--- a/modules/cms/classes/CmsObject.php
+++ b/modules/cms/classes/CmsObject.php
@@ -138,7 +138,11 @@ public static function listInTheme($theme, $skipCache = false)
$loadedItems = [];
foreach ($items as $item) {
- $loadedItems[] = static::loadCached($theme, $item);
+ $loaded = static::loadCached($theme, $item);
+ if ($loaded) {
+ $loadedItems[] = $loaded;
+ }
+ unset($loaded);
}
$result = $instance->newCollection($loadedItems);
diff --git a/modules/cms/classes/Controller.php b/modules/cms/classes/Controller.php
index 7753db63d9..dd67cc728c 100644
--- a/modules/cms/classes/Controller.php
+++ b/modules/cms/classes/Controller.php
@@ -319,6 +319,19 @@ public function runPage($page, $useAjax = true)
'session' => App::make('session'),
]);
+ /*
+ * Add global vars defined by View::share() into Twig, only if they have yet to be specified.
+ */
+ $globalVars = ViewHelper::getGlobalVars();
+ if (!empty($globalVars)) {
+ $existingGlobals = array_keys($this->getTwig()->getGlobals());
+ foreach ($globalVars as $key => $value) {
+ if (!in_array($key, $existingGlobals)) {
+ $this->getTwig()->addGlobal($key, $value);
+ }
+ }
+ }
+
/*
* Check for the presence of validation errors in the session.
*/
diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php
index 335e7833ee..bdd199ae0d 100644
--- a/modules/cms/classes/Theme.php
+++ b/modules/cms/classes/Theme.php
@@ -419,7 +419,8 @@ public function getFormConfig(): array
public function assetUrl(?string $path): string
{
$expiresAt = now()->addMinutes(Config::get('cms.urlCacheTtl', 10));
- return Cache::remember("winter.cms.{$this->dirName}.assetUrl.$path", $expiresAt, function () use ($path) {
+ $key = sprintf('winter.cms.%s.assetUrl.%s.%s', $this->dirName, request()->getSchemeAndHttpHost(), $path);
+ return Cache::remember($key, $expiresAt, function () use ($path) {
// Handle symbolized paths
if ($path && File::isPathSymbol($path)) {
return Url::asset(File::localToPublic(File::symbolizePath($path)));
diff --git a/modules/cms/lang/lv/lang.php b/modules/cms/lang/lv/lang.php
index 76e6a95375..d903336b79 100644
--- a/modules/cms/lang/lv/lang.php
+++ b/modules/cms/lang/lv/lang.php
@@ -93,6 +93,13 @@
'find_more_themes' => 'Atrast citas tēmas',
'saving' => 'Saglabā tēmu...',
'return' => 'Atgriezties tēmu sarakstā',
+ 'default_description' => 'Pielāgotā tēma, kas ģenerēta priekš :url',
+ 'scaffold' => [
+ 'label' => 'Tēmas sagatave',
+ 'empty' => 'Tukša',
+ 'less' => 'Pamata (LESS)',
+ 'tailwind' => 'Tailwind CSS',
+ ],
],
'maintenance' => [
'settings_menu' => 'Uzturēšanas režīms',
diff --git a/modules/cms/tests/classes/CmsObjectQueryTest.php b/modules/cms/tests/classes/CmsObjectQueryTest.php
index 73c06939bb..4f1ca68947 100644
--- a/modules/cms/tests/classes/CmsObjectQueryTest.php
+++ b/modules/cms/tests/classes/CmsObjectQueryTest.php
@@ -84,6 +84,8 @@ public function testLists()
"no-soft-component-class",
"optional-full-php-tags",
"optional-short-php-tags",
+ "shared-variable",
+ "shared-variable-override",
"throw-php",
"with-component",
"with-components",
diff --git a/modules/cms/tests/classes/ControllerTest.php b/modules/cms/tests/classes/ControllerTest.php
index 8899ef5c54..16d72cb9ce 100644
--- a/modules/cms/tests/classes/ControllerTest.php
+++ b/modules/cms/tests/classes/ControllerTest.php
@@ -3,10 +3,11 @@
namespace Cms\Tests\Classes;
use Cms;
+use Request;
use System\Tests\Bootstrap\TestCase;
use Cms\Classes\Theme;
use Cms\Classes\Controller;
-use Request;
+use System\Helpers\View;
use Winter\Storm\Halcyon\Model;
use Winter\Storm\Support\Facades\Config;
@@ -25,6 +26,8 @@ public function setUp(): void
Model::clearBootedModels();
Model::flushEventListeners();
+ View::clearVarCache();
+
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Archive.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/Post.php';
include_once base_path() . '/modules/system/tests/fixtures/plugins/winter/tester/components/MainMenu.php';
@@ -682,4 +685,68 @@ public function testMacro()
$response
);
}
+
+ public function testSharedVariable()
+ {
+ $this->app['view']->share('winterStatus', 'Is Awesome');
+
+ $theme = Theme::load('test');
+ $controller = new Controller($theme);
+ $response = $controller->run('/shared-variable')->getContent();
+
+ $this->assertStringContainsString(
+ '