From 829c9e3e75531e92a6f11112dc1731914b4a8c90 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Tue, 3 Dec 2024 08:48:02 +0000 Subject: [PATCH 01/56] switch revision notice to a generic class --- src/controllers/ElementsController.php | 4 ++-- src/web/assets/cp/src/css/_main.scss | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index 550b78327c6..f3afe741fb3 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -1184,10 +1184,10 @@ private function _revisionNotice($elementType): string { return Html::beginTag('div', [ - 'class' => 'revision-notice', + 'class' => 'content-notice', ]) . Html::tag('div', '', [ - 'class' => ['revision-icon'], + 'class' => ['content-notice-icon'], 'aria' => ['hidden' => 'true'], 'data' => ['icon' => 'lightbulb'], ]) . diff --git a/src/web/assets/cp/src/css/_main.scss b/src/web/assets/cp/src/css/_main.scss index 4df28b9194b..b55a41edf3c 100644 --- a/src/web/assets/cp/src/css/_main.scss +++ b/src/web/assets/cp/src/css/_main.scss @@ -1030,7 +1030,7 @@ button { } } -.revision-notice { +.content-notice { display: flex; align-items: center; place-content: stretch center; @@ -1047,7 +1047,7 @@ button { margin: 0; } - .revision-icon { + .content-notice-icon { position: relative; flex-shrink: 0; width: 34px; @@ -7808,7 +7808,8 @@ fieldset { } input:disabled, - textarea:disabled { + textarea:disabled, + select:disabled { cursor: not-allowed; } } From c2f10a76063105aec196f49ca664358267b486c9 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Tue, 3 Dec 2024 09:42:19 +0000 Subject: [PATCH 02/56] show settings > system items in readOnly mode --- src/controllers/SitesController.php | 31 +++++-- src/controllers/SystemSettingsController.php | 18 +++- src/controllers/UserSettingsController.php | 34 ++++--- src/mail/transportadapters/Gmail.php | 1 + src/mail/transportadapters/Sendmail.php | 1 + src/mail/transportadapters/Smtp.php | 1 + .../Gmail/settings.twig | 6 +- .../Sendmail/settings.twig | 3 +- .../Smtp/settings.twig | 15 ++- .../_includes/forms/checkboxSelect.twig | 3 +- src/templates/settings/email/_index.twig | 56 +++++++++--- .../settings/general/_images/image.twig | 22 +++-- src/templates/settings/general/_index.twig | 31 +++++-- src/templates/settings/index.twig | 3 +- src/templates/settings/plugins/index.twig | 91 +++++++++++-------- src/templates/settings/routes.twig | 17 +++- src/templates/settings/sites/_edit.twig | 34 +++++-- src/templates/settings/sites/index.twig | 48 ++++++---- src/templates/settings/users/_layout.twig | 6 +- .../settings/users/groups/_edit.twig | 22 +++-- .../settings/users/groups/_index.twig | 26 ++++-- .../settings/users/groups/_team.twig | 14 ++- src/templates/settings/users/settings.twig | 36 ++++++-- src/web/Controller.php | 15 ++- src/web/assets/routes/src/routes.js | 4 +- src/web/assets/routes/src/routes.scss | 6 +- src/web/twig/nodes/RequireAdminNode.php | 4 +- .../tokenparsers/RequireAdminTokenParser.php | 6 +- src/web/twig/variables/Cp.php | 48 +++++++--- 29 files changed, 425 insertions(+), 177 deletions(-) diff --git a/src/controllers/SitesController.php b/src/controllers/SitesController.php index 626e99c7983..e2bb0437f2e 100644 --- a/src/controllers/SitesController.php +++ b/src/controllers/SitesController.php @@ -17,6 +17,7 @@ use craft\web\assets\sites\SitesAsset; use craft\web\Controller; use yii\web\BadRequestHttpException; +use yii\web\ForbiddenHttpException; use yii\web\NotFoundHttpException; use yii\web\Response; use yii\web\ServerErrorHttpException; @@ -31,6 +32,8 @@ */ class SitesController extends Controller { + private bool $readOnly; + /** * @inheritdoc */ @@ -40,8 +43,16 @@ public function beforeAction($action): bool return false; } - // All actions require an admin account - $this->requireAdmin(); + // All actions require an admin account (but not allowAdminChanges) + $this->requireAdmin(false); + + $viewActions = ['settings-index', 'edit-site']; + // Most actions then require allowAdminChanges + if (!in_array($action->id, $viewActions)) { + $this->requireAdminChanges(); + } + + $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; return true; } @@ -85,11 +96,12 @@ public function actionSettingsIndex(?int $groupId = null): Response 'Delete {site}', ]); - return $this->renderTemplate('settings/sites/index.twig', compact( - 'crumbs', - 'group', - 'sites', - )); + return $this->renderTemplate('settings/sites/index.twig', [ + 'crumbs' => $crumbs, + 'group' => $group, + 'sites' => $sites, + 'readOnly' => $this->readOnly, + ]); } // Groups @@ -197,6 +209,10 @@ public function actionDeleteGroup(): Response */ public function actionEditSite(?int $siteId = null, ?Site $siteModel = null, ?int $groupId = null): Response { + if ($siteId === null && $this->readOnly) { + throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); + } + $sitesService = Craft::$app->getSites(); $brandNewSite = false; @@ -271,6 +287,7 @@ public function actionEditSite(?int $siteId = null, ?Site $siteModel = null, ?in 'site' => $siteModel, 'groupId' => $groupId, 'groupOptions' => $groupOptions, + 'readOnly' => $this->readOnly, ]); } diff --git a/src/controllers/SystemSettingsController.php b/src/controllers/SystemSettingsController.php index b8eb9f72f5b..12aa0a75bfc 100644 --- a/src/controllers/SystemSettingsController.php +++ b/src/controllers/SystemSettingsController.php @@ -38,6 +38,8 @@ */ class SystemSettingsController extends Controller { + private bool $readOnly; + /** * @inheritdoc */ @@ -47,8 +49,16 @@ public function beforeAction($action): bool return false; } - // All system setting actions require an admin - $this->requireAdmin(); + // All actions require an admin account (but not allowAdminChanges) + $this->requireAdmin(false); + + $viewActions = ['general-settings', 'edit-email-settings', 'global-set-index', 'edit-global-set']; + // Most actions then require allowAdminChanges + if (!in_array($action->id, $viewActions)) { + $this->requireAdminChanges(); + } + + $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; return true; } @@ -64,6 +74,7 @@ public function actionGeneralSettings(): Response return $this->renderTemplate('settings/general/_index.twig', [ 'system' => Craft::$app->getProjectConfig()->get('system') ?? [], + 'readOnly' => $this->readOnly, ]); } @@ -159,6 +170,7 @@ public function actionEditEmailSettings(?MailSettings $settings = null, ?Transpo 'transportTypeOptions' => $transportTypeOptions, 'allTransportAdapters' => $allTransportAdapters, 'customMailerFiles' => $customMailerFiles, + 'readOnly' => $this->readOnly, ]); } @@ -263,6 +275,7 @@ public function actionGlobalSetIndex(): Response 'buttonLabel' => Craft::t('app', 'New {type}', [ 'type' => GlobalSet::lowerDisplayName(), ]), + 'readOnly' => $this->readOnly, ]); } @@ -316,6 +329,7 @@ public function actionEditGlobalSet(?int $globalSetId = null, ?GlobalSet $global 'globalSet' => $globalSet, 'title' => $title, 'crumbs' => $crumbs, + 'readOnly' => $this->readOnly, ]); } diff --git a/src/controllers/UserSettingsController.php b/src/controllers/UserSettingsController.php index 731148dec74..47f11a9aa7b 100644 --- a/src/controllers/UserSettingsController.php +++ b/src/controllers/UserSettingsController.php @@ -25,6 +25,8 @@ */ class UserSettingsController extends Controller { + private bool $readOnly; + /** * @inheritdoc */ @@ -34,8 +36,16 @@ public function beforeAction($action): bool return false; } - // All user settings actions require an admin - $this->requireAdmin(); + // All actions require an admin account (but not allowAdminChanges) + $this->requireAdmin(false); + + $viewActions = ['edit-group']; + // Most actions then require allowAdminChanges + if (!in_array($action->id, $viewActions)) { + $this->requireAdminChanges(); + } + + $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; if ($action->id !== 'save-user-settings') { Craft::$app->requireEdition(CmsEdition::Team); @@ -61,9 +71,10 @@ public function actionEditGroup(?int $groupId = null, ?UserGroup $group = null): $group = Craft::$app->getUserGroups()->getTeamGroup(); } - return $this->renderTemplate('settings/users/groups/_team.twig', compact( - 'group', - )); + return $this->renderTemplate('settings/users/groups/_team.twig', [ + 'group' => $group, + 'readOnly' => $this->readOnly, + ]); } if (!$group) { @@ -98,12 +109,13 @@ public function actionEditGroup(?int $groupId = null, ?UserGroup $group = null): $title = Craft::t('app', 'Create a new user group'); } - return $this->renderTemplate('settings/users/groups/_edit.twig', compact( - 'group', - 'crumbs', - 'formActions', - 'title', - )); + return $this->renderTemplate('settings/users/groups/_edit.twig', [ + 'group' => $group, + 'crumbs' => $crumbs, + 'formActions' => $formActions, + 'title' => $title, + 'readOnly' => $this->readOnly, + ]); } /** diff --git a/src/mail/transportadapters/Gmail.php b/src/mail/transportadapters/Gmail.php index ac551c54666..4da3e26493c 100644 --- a/src/mail/transportadapters/Gmail.php +++ b/src/mail/transportadapters/Gmail.php @@ -83,6 +83,7 @@ public function getSettingsHtml(): ?string { return Craft::$app->getView()->renderTemplate('_components/mailertransportadapters/Gmail/settings.twig', [ 'adapter' => $this, + 'disabled' => !Craft::$app->getConfig()->getGeneral()->allowAdminChanges, ]); } diff --git a/src/mail/transportadapters/Sendmail.php b/src/mail/transportadapters/Sendmail.php index 25f51da7e02..ae5426c0200 100644 --- a/src/mail/transportadapters/Sendmail.php +++ b/src/mail/transportadapters/Sendmail.php @@ -108,6 +108,7 @@ public function getSettingsHtml(): ?string return Craft::$app->getView()->renderTemplate('_components/mailertransportadapters/Sendmail/settings.twig', [ 'adapter' => $this, 'commandOptions' => $commandOptions, + 'disabled' => !Craft::$app->getConfig()->getGeneral()->allowAdminChanges, ]); } diff --git a/src/mail/transportadapters/Smtp.php b/src/mail/transportadapters/Smtp.php index f8a68674ae4..7ef03b4cb13 100644 --- a/src/mail/transportadapters/Smtp.php +++ b/src/mail/transportadapters/Smtp.php @@ -125,6 +125,7 @@ public function getSettingsHtml(): ?string { return Craft::$app->getView()->renderTemplate('_components/mailertransportadapters/Smtp/settings.twig', [ 'adapter' => $this, + 'disabled' => !Craft::$app->getConfig()->getGeneral()->allowAdminChanges, ]); } diff --git a/src/templates/_components/mailertransportadapters/Gmail/settings.twig b/src/templates/_components/mailertransportadapters/Gmail/settings.twig index d9a79bef626..e96b7830610 100644 --- a/src/templates/_components/mailertransportadapters/Gmail/settings.twig +++ b/src/templates/_components/mailertransportadapters/Gmail/settings.twig @@ -6,7 +6,8 @@ name: 'username', suggestEnvVars: true, value: adapter.username, - errors: adapter.getErrors('username') + errors: adapter.getErrors('username'), + disabled: disabled, }) }} {{ forms.autosuggestField({ @@ -15,5 +16,6 @@ name: 'password', suggestEnvVars: true, value: adapter.password, - errors: adapter.getErrors('password') + errors: adapter.getErrors('password'), + disabled: disabled, }) }} diff --git a/src/templates/_components/mailertransportadapters/Sendmail/settings.twig b/src/templates/_components/mailertransportadapters/Sendmail/settings.twig index 0760f59710b..de82190080b 100644 --- a/src/templates/_components/mailertransportadapters/Sendmail/settings.twig +++ b/src/templates/_components/mailertransportadapters/Sendmail/settings.twig @@ -9,5 +9,6 @@ includeEnvVars: true, allowedEnvValues: null, value: adapter.command, - errors: adapter.getErrors('command') + errors: adapter.getErrors('command'), + disabled: disabled ?? false, }) }} diff --git a/src/templates/_components/mailertransportadapters/Smtp/settings.twig b/src/templates/_components/mailertransportadapters/Smtp/settings.twig index ebbf171fd42..d1f5f99d12a 100644 --- a/src/templates/_components/mailertransportadapters/Smtp/settings.twig +++ b/src/templates/_components/mailertransportadapters/Smtp/settings.twig @@ -7,7 +7,8 @@ suggestEnvVars: true, value: adapter.host, required: true, - errors: adapter.getErrors('host') + errors: adapter.getErrors('host'), + disabled: disabled, }) }} {{ forms.autosuggestField({ @@ -17,7 +18,8 @@ suggestEnvVars: true, value: adapter.port, size: 20, - errors: adapter.getErrors('port') + errors: adapter.getErrors('port'), + disabled: disabled, }) }} {{ forms.booleanMenuField({ @@ -26,7 +28,8 @@ name: 'useAuthentication', includeEnvVars: true, value: adapter.useAuthentication, - toggle: 'auth-credentials' + toggle: 'auth-credentials', + disabled: disabled, }) }}
@@ -36,7 +39,8 @@ name: 'username', suggestEnvVars: true, value: adapter.username, - errors: adapter.getErrors('username') + errors: adapter.getErrors('username'), + disabled: disabled, }) }} {{ forms.autosuggestField({ @@ -45,6 +49,7 @@ name: 'password', suggestEnvVars: true, value: adapter.password, - errors: adapter.getErrors('password') + errors: adapter.getErrors('password'), + disabled: disabled, }) }}
diff --git a/src/templates/_includes/forms/checkboxSelect.twig b/src/templates/_includes/forms/checkboxSelect.twig index d0371d6c5dc..3a028f0a866 100644 --- a/src/templates/_includes/forms/checkboxSelect.twig +++ b/src/templates/_includes/forms/checkboxSelect.twig @@ -31,6 +31,7 @@ checked: allChecked, autofocus: (autofocus ?? false) and not craft.app.request.isMobileBrowser(true), targetPrefix: targetPrefix ?? null, + disabled: disabled } only %} {%- elseif name is defined and (name|length < 3 or name|slice(-2) != '[]') %} @@ -56,7 +57,7 @@ {% include "_includes/forms/checkbox" with { name: (name ?? false) ? "#{name}[]" : null, checked: selected, - disabled: (showAllOption and allChecked), + disabled: (showAllOption and allChecked) or disabled, targetPrefix: targetPrefix ?? null, }|merge(option) only %} diff --git a/src/templates/settings/email/_index.twig b/src/templates/settings/email/_index.twig index f5f9781c18c..5178646308c 100644 --- a/src/templates/settings/email/_index.twig +++ b/src/templates/settings/email/_index.twig @@ -1,9 +1,14 @@ -{% requireAdmin %} +{% requireAdmin '0' %} {% extends '_layouts/cp.twig' %} {% set title = 'Email Settings'|t('app') %} -{% set fullPageForm = true %} + +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} + +{% set fullPageForm = not readOnly %} {% set crumbs = [ { label: "Settings"|t('app'), url: url('settings') } @@ -33,6 +38,10 @@ {% endif %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} + {% block content %} {% if customMailerFiles|length %}
@@ -47,8 +56,10 @@
{% endif %} - {{ actionInput('system-settings/save-email-settings') }} - {{ redirectInput('settings') }} + {% if not readOnly %} + {{ actionInput('system-settings/save-email-settings') }} + {{ redirectInput('settings') }} + {% endif %} {{ forms.autosuggestField({ first: true, @@ -60,7 +71,8 @@ value: settings.fromEmail, autofocus: true, required: true, - errors: (freshSettings ? null : settings.getErrors('fromEmail')) + errors: (freshSettings ? null : settings.getErrors('fromEmail')), + disabled: readOnly, }) }} {{ forms.autosuggestField({ @@ -71,7 +83,8 @@ name: 'replyToEmail', suggestEnvVars: true, value: settings.replyToEmail, - errors: (freshSettings ? null : settings.getErrors('replyToEmail')) + errors: (freshSettings ? null : settings.getErrors('replyToEmail')), + disabled: readOnly, }) }} {{ forms.autosuggestField({ @@ -82,7 +95,8 @@ suggestEnvVars: true, value: settings.fromName, required: true, - errors: (freshSettings ? null : settings.getErrors('fromName')) + errors: (freshSettings ? null : settings.getErrors('fromName')), + disabled: readOnly, }) }} {% if CraftEdition >= CraftPro %} @@ -94,7 +108,8 @@ suggestions: craft.cp.getTemplateSuggestions(), suggestEnvVars: true, value: settings.template, - errors: (freshSettings ? null : settings.getErrors('template')) + errors: (freshSettings ? null : settings.getErrors('template')), + disabled: readOnly, }) }} {% endif %} @@ -108,22 +123,35 @@ options: transportTypeOptions, value: className(adapter), errors: adapter.getErrors('type') ?? null, - toggle: true + toggle: true, + disabled: readOnly, }) }} {% for _adapter in allTransportAdapters %} {% set isCurrent = (className(_adapter) == className(adapter)) %} -
- - {% if isImageUploaded %} -
- - -
+ {% if disabled is defined and disabled == true %} + {# don't show any of that #} {% else %} -
- -
+ + {% if isImageUploaded %} +
+ + +
+ {% else %} +
+ +
+ {% endif %} {% endif %}
diff --git a/src/templates/settings/general/_index.twig b/src/templates/settings/general/_index.twig index 42285e3d6e6..683c653dc47 100644 --- a/src/templates/settings/general/_index.twig +++ b/src/templates/settings/general/_index.twig @@ -1,7 +1,10 @@ {% extends "_layouts/cp" %} {% import "_includes/forms" as forms %} {% set title = "General Settings"|t('app') %} -{% set fullPageForm = true %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} +{% set fullPageForm = not readOnly %} {% set crumbs = [ { label: "Settings"|t('app'), url: url('settings') } @@ -33,10 +36,15 @@ {% from _self import configWarning %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} {% block content %} - {{ actionInput('system-settings/save-general-settings') }} - {{ redirectInput('settings') }} + {% if not readOnly %} + {{ actionInput('system-settings/save-general-settings') }} + {{ redirectInput('settings') }} + {% endif %} {{ forms.autosuggestField({ first: true, @@ -44,7 +52,8 @@ id: 'name', suggestEnvVars: true, name: 'name', - value: system.name + value: system.name, + disabled: readOnly, }) }} {{ forms.booleanMenuField({ @@ -55,7 +64,8 @@ yesLabel: 'Online'|t('app'), noLabel: 'Offline'|t('app'), includeEnvVars: true, - value: system.live + value: system.live, + disabled: readOnly, }) }} {{ forms.textField({ @@ -66,6 +76,7 @@ value: system.retryDuration, inputmode: 'numeric', size: 4, + disabled: readOnly, }) }} {{ forms.timeZoneField({ @@ -75,6 +86,7 @@ name: 'timeZone', value: system.timeZone, includeEnvVars: true, + disabled: readOnly, }) }} {% if CraftEdition >= CraftPro %} @@ -88,13 +100,14 @@ {{ forms.field({ label: "Login Page Logo"|t('app'), - instructions: "SVG file recommended. The logo will be displayed at {size} wide."|t('app', { size: '300px' }) - }, include('settings/general/_images/logo.twig')) }} + instructions: "SVG file recommended. The logo will be displayed at {size} wide."|t('app', { size: '300px' }), + }, include('settings/general/_images/logo.twig', {'disabled': readOnly})) }} {{ forms.field({ label: "Site Icon"|t('app'), - instructions: "Square SVG file recommended. The logo will be displayed at {size} by {size}."|t('app', { size: '32px' }) - }, include('settings/general/_images/icon.twig')) }} + instructions: "Square SVG file recommended. The logo will be displayed at {size} by {size}."|t('app', { size: '32px' }), + disabled: readOnly, + }, include('settings/general/_images/icon.twig', {'disabled': readOnly})) }}
{% endif %} diff --git a/src/templates/settings/index.twig b/src/templates/settings/index.twig index 08577d725c9..18972abb630 100644 --- a/src/templates/settings/index.twig +++ b/src/templates/settings/index.twig @@ -1,4 +1,5 @@ -{% requireAdmin %} +{% requireAdmin '0' %} + {% extends "_layouts/cp" %} {% set title = "Settings"|t('app') %} diff --git a/src/templates/settings/plugins/index.twig b/src/templates/settings/plugins/index.twig index 1c56e53d1a7..2f77629e248 100644 --- a/src/templates/settings/plugins/index.twig +++ b/src/templates/settings/plugins/index.twig @@ -1,7 +1,11 @@ -{% requireAdmin %} +{% requireAdmin '0' %} {% extends "_layouts/cp" %} {% set title = "Plugins"|t('app') %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} + {% do view.registerAssetBundle("craft\\web\\assets\\plugins\\PluginsAsset") %} {% set crumbs = [ @@ -93,18 +97,27 @@ {% set showLicenseKey = config.licenseKey or config.licenseKeyStatus != 'unknown' %}
- +
- {{ tag('a', { - text: 'Buy now'|t('app'), - class: [ - 'btn', - config.licenseIssues is not empty ? 'submit', - config.licenseKeyStatus != 'trial' ? 'hidden', - ]|filter, - href: url('plugin-store/buy/'~handle~'/'~config.edition), - }) }} - + {% if not readOnly %} + {{ tag('a', { + text: 'Buy now'|t('app'), + class: [ + 'btn', + config.licenseIssues is not empty ? 'submit', + config.licenseKeyStatus != 'trial' ? 'hidden', + ]|filter, + href: url('plugin-store/buy/'~handle~'/'~config.edition), + }) }} + + {% endif %}
{% for issue in config.licenseIssues %}

@@ -143,34 +156,36 @@ {{ "Not installed"|t('app') }} {% endif %} - -

- {{ hiddenInput('pluginHandle', handle) }} - {{ csrfInput() }} - -
+ + + {% endif %} {% endfor %} diff --git a/src/templates/settings/routes.twig b/src/templates/settings/routes.twig index dc7d7c3d4b8..ccc51c3bb5a 100644 --- a/src/templates/settings/routes.twig +++ b/src/templates/settings/routes.twig @@ -1,10 +1,15 @@ -{% requireAdmin %} +{% requireAdmin '0' %} {% extends "_layouts/cp" %} {% set title = "Routes"|t('app') %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} {% block actionButton %} - + {% if not readOnly %} + + {% endif %} {% endblock %} {% set crumbs = [ @@ -32,15 +37,19 @@ {% set routes = craft.routes.getProjectConfigRoutes() %} - {% block main %}
+ {% if readOnly %} +
+ {{ craft.cp.allowAdminChangesReadOnlyNotice()|raw }} +
+ {% endif %} {% for route in routes %} -
+
{%- apply spaceless %} {% if craft.app.getIsMultiSite() %} diff --git a/src/templates/settings/sites/_edit.twig b/src/templates/settings/sites/_edit.twig index ec4c1f9d6e5..feb32ae1a42 100644 --- a/src/templates/settings/sites/_edit.twig +++ b/src/templates/settings/sites/_edit.twig @@ -1,6 +1,9 @@ {% extends '_layouts/cp.twig' %} -{% set fullPageForm = true %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} +{% set fullPageForm = not readOnly %} {% set formActions = [ { @@ -13,11 +16,16 @@ {% import '_includes/forms.twig' as forms %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} {% block content %} - {{ actionInput('sites/save-site') }} - {{ redirectInput('settings/sites') }} - {% if site.id %}{{ hiddenInput('siteId', site.id) }}{% endif %} + {% if not readOnly %} + {{ actionInput('sites/save-site') }} + {{ redirectInput('settings/sites') }} + {% if site.id %}{{ hiddenInput('siteId', site.id) }}{% endif %} + {% endif %} {{ forms.selectField({ first: true, @@ -27,7 +35,8 @@ id: 'group', name: 'group', options: groupOptions, - value: groupId + value: groupId, + disabled: readOnly, }) }}
@@ -42,6 +51,7 @@ errors: site.getErrors('name'), autofocus: true, required: true, + disabled: readOnly, }) }} {{ forms.textField({ @@ -54,7 +64,8 @@ autocapitalize: false, value: site.handle, errors: site.getErrors('handle'), - required: true + required: true, + disabled: readOnly, }) }} {{ forms.languageMenuField({ @@ -66,6 +77,7 @@ options: craft.cp.getLanguageOptions(true), errors: site.getErrors('language'), includeEnvVars: true, + disabled: readOnly, }) }} {% if (craft.app.isMultiSite or not site.id) %} @@ -79,6 +91,7 @@ value: site.getEnabled(false), disabled: site.primary, tip: site.primary ? 'The primary site cannot be disabled.'|t('app') : null, + disabled: readOnly, }) }} {% endif %} @@ -88,7 +101,8 @@ instructions: "The primary site will be loaded by default on the front end."|t('app'), id: 'primary', name: 'primary', - on: site.primary + on: site.primary, + disabled: readOnly, }) }} {% else %} {{ hiddenInput('primary', '1') }} @@ -101,7 +115,8 @@ id: 'has-urls', name: 'hasUrls', on: site.hasUrls, - toggle: 'url-settings' + toggle: 'url-settings', + disabled: readOnly, }) }}
@@ -114,7 +129,8 @@ suggestAliases: true, name: 'baseUrl', value: site.getBaseUrl(false), - errors: site.getErrors('baseUrl') + errors: site.getErrors('baseUrl'), + disabled: readOnly, }) }}
diff --git a/src/templates/settings/sites/index.twig b/src/templates/settings/sites/index.twig index 0cd6ade730c..720b3441897 100644 --- a/src/templates/settings/sites/index.twig +++ b/src/templates/settings/sites/index.twig @@ -1,16 +1,21 @@ {% extends "_layouts/cp" %} {% set title = "Sites"|t('app') %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} -{% set multiple = (sites|length > 1) %} -{% set canSort = group and multiple %} +{% set multiple = (sites|length > 1) and not readOnly %} +{% set canSort = group and multiple and not readOnly %} {% block actionButton %} - {{ tag('a', { - href: url('settings/sites/new', (group ? { groupId: group.id } : null)), - class: ['btn', 'submit', 'add', 'icon', craft.app.sites.getRemainingSites() ? null : 'disabled'], - text: "New site"|t('app'), - }) }} + {% if not readOnly %} + {{ tag('a', { + href: url('settings/sites/new', (group ? { groupId: group.id } : null)), + class: ['btn', 'submit', 'add', 'icon', craft.app.sites.getRemainingSites() ? null : 'disabled'], + text: "New site"|t('app'), + }) }} + {% endif %} {% endblock %} @@ -34,21 +39,26 @@ -
- + {% if not readOnly %} +
+ - {% if group %} - - - {% endif %} -
+ {% if group %} + + + {% endif %} +
+ {% endif %} {% endblock %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} {% block content %} {% if sites|length %} diff --git a/src/templates/settings/users/_layout.twig b/src/templates/settings/users/_layout.twig index 8b2bbd18920..5965086478c 100644 --- a/src/templates/settings/users/_layout.twig +++ b/src/templates/settings/users/_layout.twig @@ -1,4 +1,4 @@ -{% requireAdmin %} +{% requireAdmin '0' %} {% extends "_layouts/cp" %} {% set title = "User Settings"|t('app') %} @@ -10,9 +10,11 @@ {% set navItems = { teamGroup: CraftEdition == CraftTeam ? { label: "User Permissions"|t('app'), url: url('settings/users') }, groups: CraftEdition >= CraftPro ? { label: "User Groups"|t('app'), url: url('settings/users') }, - fields: { label: "User Profile Fields"|t('app'), url: url('settings/users/fields') }, settings: { label: "Settings"|t('app'), url: url('settings/users/settings') } }|filter %} +{% if not readOnly %} + {% set navItems = navItems|merge({fields: { label: "User Profile Fields"|t('app'), url: url('settings/users/fields') },}) %} +{% endif %} {% set docTitle = navItems[selectedNavItem].label~' - '~title %} diff --git a/src/templates/settings/users/groups/_edit.twig b/src/templates/settings/users/groups/_edit.twig index 720e6525bd2..e12e4d69360 100644 --- a/src/templates/settings/users/groups/_edit.twig +++ b/src/templates/settings/users/groups/_edit.twig @@ -1,13 +1,17 @@ {% extends "_layouts/cp" %} {% import "_includes/forms" as forms %} -{% set fullPageForm = true %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} +{% set fullPageForm = not readOnly %} {% block content %} - {{ actionInput('user-settings/save-group') }} - {{ redirectInput('settings/users/groups/{id}') }} - - {% if group.id %} - {{ hiddenInput('groupId', group.id) }} + {% if not readOnly %} + {{ actionInput('user-settings/save-group') }} + {{ redirectInput('settings/users/groups/{id}') }} + {% if group.id %} + {{ hiddenInput('groupId', group.id) }} + {% endif %} {% endif %} {{ forms.textField({ @@ -19,6 +23,7 @@ errors: group.getErrors('name'), autofocus: true, required: true, + disabled: readOnly, }) }} {{ forms.textField({ @@ -30,7 +35,8 @@ autocapitalize: false, value: group.handle, errors: group.getErrors('handle'), - required: true + required: true, + disabled: readOnly, }) }} {{ forms.textareaField({ @@ -40,6 +46,7 @@ name: 'description', value: group.description ?? null, errors: group.getErrors('description'), + disabled: readOnly, }) }}
@@ -51,6 +58,7 @@ subject: group ?? null, permissions: craft.app.userPermissions.getAllPermissions(), includeGroupPlaceholder: not group.id, + disabled: readOnly, } only %}
{% endblock %} diff --git a/src/templates/settings/users/groups/_index.twig b/src/templates/settings/users/groups/_index.twig index 42cf14c5ba6..68cb3696952 100644 --- a/src/templates/settings/users/groups/_index.twig +++ b/src/templates/settings/users/groups/_index.twig @@ -1,8 +1,11 @@ {% requireEdition CraftPro %} -{% requireAdmin %} +{% requireAdmin '0' %} {% extends "settings/users/_layout" %} {% set selectedNavItem = 'groups' %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} @@ -14,12 +17,18 @@ {% set groups = craft.app.userGroups.getAllGroups() %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} + {% block content %}
- + {% if not readOnly %} + + {% endif %} {% endblock %} {% set tableData = [] %} @@ -39,11 +48,14 @@ { name: '__slot:handle', title: Craft.t('app', 'Handle') } ]; - new Craft.VueAdminTable({ + var config = { columns: columns, container: '#groups-vue-admin-table', - deleteAction: 'user-settings/delete-group', emptyMessage: Craft.t('app', 'No groups exist yet.'), tableData: {{ tableData|json_encode|raw }} - }); + }; + if (!{{ readOnly }}) { + config['deleteAction'] = 'user-settings/delete-group'; + } + new Craft.VueAdminTable(config); {% endjs %} \ No newline at end of file diff --git a/src/templates/settings/users/groups/_team.twig b/src/templates/settings/users/groups/_team.twig index fef332e5b5e..43f0ada5788 100644 --- a/src/templates/settings/users/groups/_team.twig +++ b/src/templates/settings/users/groups/_team.twig @@ -1,17 +1,25 @@ {% extends 'settings/users/_layout' %} {% set selectedNavItem = 'teamGroup' %} {% import '_includes/forms' as forms %} -{% set fullPageForm = true %} + +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} +{% set fullPageForm = not readOnly %} + {% block content %} - {{ actionInput('user-settings/save-group') }} - {{ redirectInput('settings/users') }} + {% if not readOnly %} + {{ actionInput('user-settings/save-group') }} + {{ redirectInput('settings/users') }} + {% endif %}
{% include "_includes/permissions" with { subject: group ?? null, permissions: craft.app.userPermissions.getAllPermissions(), includeGroupPlaceholder: not group.id, + disabled: readOnly, } only %}
{% endblock %} diff --git a/src/templates/settings/users/settings.twig b/src/templates/settings/users/settings.twig index 864c329ef94..ffcbc6c2169 100644 --- a/src/templates/settings/users/settings.twig +++ b/src/templates/settings/users/settings.twig @@ -1,12 +1,14 @@ -{% requireAdmin %} +{% requireAdmin '0' %} {% extends "settings/users/_layout" %} {% set selectedNavItem = 'settings' %} -{% set fullPageForm = true %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} +{% set fullPageForm = not readOnly %} {% import "_includes/forms" as forms %} - {% if settings is not defined %} {% set settings = craft.app.projectConfig.get('users') ?? [] %} {% endif %} @@ -36,7 +38,7 @@ {% endif %} {% endfor %} -{% macro assetLocationInput(volumeOptions, photoVolume, subpath) %} +{% macro assetLocationInput(volumeOptions, photoVolume, subpath, disabled) %} {% import '_includes/forms' as forms %}
@@ -45,6 +47,7 @@ name: 'photoVolumeId', options: volumeOptions, value: photoVolume.id ?? null, + disabled: disabled, }) }}
@@ -53,15 +56,22 @@ class: 'ltr', name: 'photoSubpath', value: subpath, - placeholder: "path/to/subfolder"|t('app') + placeholder: "path/to/subfolder"|t('app'), + disabled: disabled, }) }}
{% endmacro %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} + {% block content %} - {{ actionInput('user-settings/save-user-settings') }} - {{ csrfInput() }} + {% if not readOnly %} + {{ actionInput('user-settings/save-user-settings') }} + {{ csrfInput() }} + {% endif %} {% if CraftEdition >= CraftTeam %}

{{ 'User Photos'|t('app') }}

@@ -77,7 +87,7 @@ first: true, label: "User Photo Location"|t('app'), instructions: "Where do you want to store user photos? Note that the subfolder path can contain variables like {username}."|t('app') - }, _self.assetLocationInput(volumeOptions, photoVolume, settings.photoSubpath)) }} + }, _self.assetLocationInput(volumeOptions, photoVolume, settings.photoSubpath, readOnly)) }} {% else %} {{ forms.field({ first: true, @@ -100,6 +110,7 @@ allLabel: 'All users'|t('app'), allValue: 'all', values: settings.require2fa ?? false, + disabled: readOnly, }) }} {% endif %} @@ -110,6 +121,7 @@ instructions: 'Should new email addresses be verified before getting saved to user accounts? (This also affects new user registration.)'|t('app'), name: 'requireEmailVerification', on: settings.requireEmailVerification, + disabled: readOnly, }) }}
@@ -120,7 +132,8 @@ label: 'Allow public registration'|t('app'), name: 'allowPublicRegistration', on: settings.allowPublicRegistration, - toggle: 'publicRegistrationSettings' + toggle: 'publicRegistrationSettings', + disabled: readOnly, }) }}
@@ -129,6 +142,7 @@ instructions: 'Whether custom fields should be validated during public registration.'|t('app'), name: 'validateOnPublicRegistration', on: settings.validateOnPublicRegistration, + disabled: readOnly, }) }} {{ forms.lightswitchField({ @@ -136,6 +150,7 @@ instructions: 'Should users who register their own accounts be deactivated by default? This will prevent them from receiving an activation email or logging in.'|t('app'), name: 'deactivateByDefault', on: settings.deactivateByDefault, + disabled: readOnly, }) }} {% set groups = [{ label: "None"|t('app'), value: '' }] %} @@ -148,7 +163,8 @@ instructions: "Choose a user group that publicly-registered members will be added to by default."|t('app'), name: 'defaultGroup', options: groups, - value: settings.defaultGroup + value: settings.defaultGroup, + disabled: readOnly, }) }}
{% endif %} diff --git a/src/web/Controller.php b/src/web/Controller.php index bd17b24182d..36a7a3e0442 100644 --- a/src/web/Controller.php +++ b/src/web/Controller.php @@ -493,7 +493,20 @@ public function requireAdmin(bool $requireAdminChanges = true): void } // Make sure admin changes are allowed - if ($requireAdminChanges && !Craft::$app->getConfig()->getGeneral()->allowAdminChanges) { + if ($requireAdminChanges) { + $this->requireAdminChanges(); + } + } + + /** + * Throws a 403 error if the config setting is disabled. + * + * @throws ForbiddenHttpException if the current user is not an admin + */ + protected function requireAdminChanges(): void + { + // Make sure admin changes are allowed + if (!Craft::$app->getConfig()->getGeneral()->allowAdminChanges) { throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); } } diff --git a/src/web/assets/routes/src/routes.js b/src/web/assets/routes/src/routes.js index 24175550782..073bc0d5fd5 100644 --- a/src/web/assets/routes/src/routes.js +++ b/src/web/assets/routes/src/routes.js @@ -79,7 +79,9 @@ import './routes.scss'; this.$uri = this.$container.find('.uri:first'); this.$template = this.$container.find('.template:first'); - this.addListener(this.$container, 'click', 'edit'); + if (Craft.allowAdminChanges) { + this.addListener(this.$container, 'click', 'edit'); + } }, edit: function () { diff --git a/src/web/assets/routes/src/routes.scss b/src/web/assets/routes/src/routes.scss index 5ea8a610a38..2ded26d64fa 100644 --- a/src/web/assets/routes/src/routes.scss +++ b/src/web/assets/routes/src/routes.scss @@ -16,7 +16,7 @@ background-color: var(--gray-050); cursor: pointer; - &:hover::after { + &:hover:not(.inactive)::after { position: absolute; inset-block-start: 9px; @include right(5px); @@ -25,6 +25,10 @@ color: var(--link-color); } + &.inactive:hover { + cursor: not-allowed; + } + .uri-container, .template { padding-block: 7px; diff --git a/src/web/twig/nodes/RequireAdminNode.php b/src/web/twig/nodes/RequireAdminNode.php index a9691a1917d..d6efdf97f57 100644 --- a/src/web/twig/nodes/RequireAdminNode.php +++ b/src/web/twig/nodes/RequireAdminNode.php @@ -26,6 +26,8 @@ public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write(Craft::class . "::\$app->controller->requireAdmin();\n"); + ->write(Craft::class . "::\$app->controller->requireAdmin(") + ->subcompile($this->getNode('requireAdminChanges')) + ->raw(");\n"); } } diff --git a/src/web/twig/tokenparsers/RequireAdminTokenParser.php b/src/web/twig/tokenparsers/RequireAdminTokenParser.php index c3deaab2125..35f9d423c12 100644 --- a/src/web/twig/tokenparsers/RequireAdminTokenParser.php +++ b/src/web/twig/tokenparsers/RequireAdminTokenParser.php @@ -28,9 +28,13 @@ public function parse(Token $token): RequireAdminNode $parser = $this->parser; $stream = $parser->getStream(); + $nodes = [ + 'requireAdminChanges' => $parser->getExpressionParser()->parseExpression(), + ]; + $stream->expect(Token::BLOCK_END_TYPE); - return new RequireAdminNode([], [], $lineno, $this->getTag()); + return new RequireAdminNode($nodes, [], $lineno, $this->getTag()); } /** diff --git a/src/web/twig/variables/Cp.php b/src/web/twig/variables/Cp.php index 9e191897377..803a072606b 100644 --- a/src/web/twig/variables/Cp.php +++ b/src/web/twig/variables/Cp.php @@ -18,6 +18,7 @@ use craft\helpers\ArrayHelper; use craft\helpers\Assets; use craft\helpers\Cp as CpHelper; +use craft\helpers\Html; use craft\helpers\Inflector; use craft\helpers\StringHelper; use craft\helpers\UrlHelper; @@ -325,13 +326,11 @@ public function nav(): array } if ($isAdmin) { - if ($generalConfig->allowAdminChanges) { - $navItems[] = [ - 'url' => 'settings', - 'label' => Craft::t('app', 'Settings'), - 'icon' => 'gear', - ]; - } + $navItems[] = [ + 'url' => 'settings', + 'label' => Craft::t('app', 'Settings'), + 'icon' => 'gear', + ]; $navItems[] = [ 'url' => 'plugin-store', @@ -422,10 +421,12 @@ public function settings(): array 'iconMask' => '@app/icons/light/user-group.svg', 'label' => Craft::t('app', 'Users'), ]; - $settings[$label]['addresses'] = [ - 'iconMask' => '@app/icons/light/map-location.svg', - 'label' => Craft::t('app', 'Addresses'), - ]; + if (Craft::$app->getConfig()->getGeneral()->allowAdminChanges) { + $settings[$label]['addresses'] = [ + 'iconMask' => '@app/icons/light/map-location.svg', + 'label' => Craft::t('app', 'Addresses'), + ]; + } $settings[$label]['email'] = [ 'iconMask' => '@app/icons/light/envelope.svg', 'label' => Craft::t('app', 'Email'), @@ -1096,4 +1097,29 @@ public function fieldLayoutDesigner(FieldLayout $fieldLayout, array $config = [] { return CpHelper::fieldLayoutDesignerHtml($fieldLayout, $config); } + + /** + * Returns the notice that should show when admin is viewing the available settings pages + * while `allowAdminChanges` is set to false. + * + * @return string + * @since 5.6.0 + */ + public function allowAdminChangesReadOnlyNotice(): string + { + return + Html::beginTag('div', [ + 'class' => 'content-notice', + ]) . + Html::tag('div', '', [ + 'class' => ['content-notice-icon'], + 'aria' => ['hidden' => 'true'], + 'data' => ['icon' => 'lightbulb'], + ]) . + Html::tag('p', Craft::t( + 'app', + '`allowAdminChanges` is off. You can view the settings, but not change them.', + )) . + Html::endTag('div'); + } } From 429121ba5501010cc01317a7331cefea98f733d8 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Tue, 3 Dec 2024 11:10:28 +0000 Subject: [PATCH 03/56] no need for a new method... --- src/controllers/SitesController.php | 12 ++++++------ src/controllers/SystemSettingsController.php | 12 ++++++------ src/controllers/UserSettingsController.php | 11 ++++++----- src/web/Controller.php | 15 +-------------- 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/src/controllers/SitesController.php b/src/controllers/SitesController.php index e2bb0437f2e..bf35bee21b7 100644 --- a/src/controllers/SitesController.php +++ b/src/controllers/SitesController.php @@ -43,13 +43,13 @@ public function beforeAction($action): bool return false; } - // All actions require an admin account (but not allowAdminChanges) - $this->requireAdmin(false); - $viewActions = ['settings-index', 'edit-site']; - // Most actions then require allowAdminChanges - if (!in_array($action->id, $viewActions)) { - $this->requireAdminChanges(); + if (in_array($action->id, $viewActions)) { + // Some actions require admin but not allowAdminChanges + $this->requireAdmin(false); + } else { + // All other actions require an admin & allowAdminChanges + $this->requireAdmin(); } $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; diff --git a/src/controllers/SystemSettingsController.php b/src/controllers/SystemSettingsController.php index 12aa0a75bfc..8738e2951bb 100644 --- a/src/controllers/SystemSettingsController.php +++ b/src/controllers/SystemSettingsController.php @@ -49,13 +49,13 @@ public function beforeAction($action): bool return false; } - // All actions require an admin account (but not allowAdminChanges) - $this->requireAdmin(false); - $viewActions = ['general-settings', 'edit-email-settings', 'global-set-index', 'edit-global-set']; - // Most actions then require allowAdminChanges - if (!in_array($action->id, $viewActions)) { - $this->requireAdminChanges(); + if (in_array($action->id, $viewActions)) { + // Some actions require admin but not allowAdminChanges + $this->requireAdmin(false); + } else { + // All other actions require an admin & allowAdminChanges + $this->requireAdmin(); } $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; diff --git a/src/controllers/UserSettingsController.php b/src/controllers/UserSettingsController.php index 47f11a9aa7b..c063b1e6782 100644 --- a/src/controllers/UserSettingsController.php +++ b/src/controllers/UserSettingsController.php @@ -36,13 +36,14 @@ public function beforeAction($action): bool return false; } - // All actions require an admin account (but not allowAdminChanges) - $this->requireAdmin(false); $viewActions = ['edit-group']; - // Most actions then require allowAdminChanges - if (!in_array($action->id, $viewActions)) { - $this->requireAdminChanges(); + if (in_array($action->id, $viewActions)) { + // Some actions require admin but not allowAdminChanges + $this->requireAdmin(false); + } else { + // All other actions require an admin & allowAdminChanges + $this->requireAdmin(); } $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; diff --git a/src/web/Controller.php b/src/web/Controller.php index 36a7a3e0442..bd17b24182d 100644 --- a/src/web/Controller.php +++ b/src/web/Controller.php @@ -493,20 +493,7 @@ public function requireAdmin(bool $requireAdminChanges = true): void } // Make sure admin changes are allowed - if ($requireAdminChanges) { - $this->requireAdminChanges(); - } - } - - /** - * Throws a 403 error if the config setting is disabled. - * - * @throws ForbiddenHttpException if the current user is not an admin - */ - protected function requireAdminChanges(): void - { - // Make sure admin changes are allowed - if (!Craft::$app->getConfig()->getGeneral()->allowAdminChanges) { + if ($requireAdminChanges && !Craft::$app->getConfig()->getGeneral()->allowAdminChanges) { throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); } } From e8b7b0bcfc0615872bd1dbcf8d1a7a2446c67690 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Tue, 3 Dec 2024 11:19:46 +0000 Subject: [PATCH 04/56] show user profile fields but disabled --- src/templates/settings/users/_layout.twig | 4 +--- src/templates/settings/users/fields.twig | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/templates/settings/users/_layout.twig b/src/templates/settings/users/_layout.twig index 5965086478c..66cb0d0b68c 100644 --- a/src/templates/settings/users/_layout.twig +++ b/src/templates/settings/users/_layout.twig @@ -10,11 +10,9 @@ {% set navItems = { teamGroup: CraftEdition == CraftTeam ? { label: "User Permissions"|t('app'), url: url('settings/users') }, groups: CraftEdition >= CraftPro ? { label: "User Groups"|t('app'), url: url('settings/users') }, + fields: { label: "User Profile Fields"|t('app'), url: url('settings/users/fields') }, settings: { label: "Settings"|t('app'), url: url('settings/users/settings') } }|filter %} -{% if not readOnly %} - {% set navItems = navItems|merge({fields: { label: "User Profile Fields"|t('app'), url: url('settings/users/fields') },}) %} -{% endif %} {% set docTitle = navItems[selectedNavItem].label~' - '~title %} diff --git a/src/templates/settings/users/fields.twig b/src/templates/settings/users/fields.twig index b5d7425976a..275a1cc37c8 100644 --- a/src/templates/settings/users/fields.twig +++ b/src/templates/settings/users/fields.twig @@ -1,9 +1,15 @@ -{% requireAdmin %} +{% requireAdmin '0' %} {% extends "settings/users/_layout" %} {% set selectedNavItem = 'fields' %} {% import "_includes/forms" as forms %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} {% block content %}
@@ -14,10 +20,13 @@ first: true, fieldLayout: fieldLayout ?? craft.app.fields.getLayoutByType('craft\\elements\\User'), withCardViewDesigner: true, + disabled: readOnly, }) }} -
- -
+ {% if not readOnly %} +
+ +
+ {% endif %}
{% endblock %} From e0e49526c64647b70f2418a3696e56fa041871c6 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Tue, 3 Dec 2024 11:35:22 +0000 Subject: [PATCH 05/56] show settings > content items in readOnly mode --- src/controllers/CategoriesController.php | 12 +- src/controllers/EntryTypesController.php | 52 +++- src/controllers/FieldsController.php | 259 ++++++++++-------- src/controllers/SectionsController.php | 21 +- src/controllers/SystemSettingsController.php | 5 + src/controllers/TagsController.php | 14 +- src/helpers/Cp.php | 31 ++- .../_includes/forms/autosuggest.twig | 1 - .../_includes/forms/checkboxSelect.twig | 5 +- src/templates/settings/categories/_edit.twig | 28 +- src/templates/settings/categories/index.twig | 24 +- src/templates/settings/entry-types/_edit.twig | 18 +- src/templates/settings/entry-types/index.twig | 35 ++- src/templates/settings/fields/_edit.twig | 19 +- src/templates/settings/fields/index.twig | 26 +- src/templates/settings/globals/_edit.twig | 20 +- src/templates/settings/globals/_index.twig | 31 ++- src/templates/settings/sections/_edit.twig | 57 ++-- src/templates/settings/sections/_index.twig | 24 +- src/templates/settings/tags/_edit.twig | 22 +- src/templates/settings/tags/index.twig | 22 +- src/web/assets/cp/src/css/_main.scss | 2 +- .../assets/cp/src/js/FieldLayoutDesigner.js | 6 +- src/web/twig/variables/Cp.php | 15 +- 24 files changed, 512 insertions(+), 237 deletions(-) diff --git a/src/controllers/CategoriesController.php b/src/controllers/CategoriesController.php index 22afb46b3cc..1f340243766 100644 --- a/src/controllers/CategoriesController.php +++ b/src/controllers/CategoriesController.php @@ -54,12 +54,13 @@ class CategoriesController extends Controller */ public function actionGroupIndex(): Response { - $this->requireAdmin(); + $this->requireAdmin(false); $groups = Craft::$app->getCategories()->getAllGroups(); return $this->renderTemplate('settings/categories/index.twig', [ 'categoryGroups' => $groups, + 'readOnly' => !Craft::$app->getConfig()->getGeneral()->allowAdminChanges, ]); } @@ -73,7 +74,13 @@ public function actionGroupIndex(): Response */ public function actionEditCategoryGroup(?int $groupId = null, ?CategoryGroup $categoryGroup = null): Response { - $this->requireAdmin(); + $this->requireAdmin(false); + + $readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; + + if ($groupId === null && $readOnly) { + throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); + } $variables = []; @@ -112,6 +119,7 @@ public function actionEditCategoryGroup(?int $groupId = null, ?CategoryGroup $ca $variables['groupId'] = $groupId; $variables['categoryGroup'] = $categoryGroup; + $variables['readOnly'] = $readOnly; return $this->renderTemplate('settings/categories/_edit.twig', $variables); } diff --git a/src/controllers/EntryTypesController.php b/src/controllers/EntryTypesController.php index f029a9b05b8..739c688f44e 100644 --- a/src/controllers/EntryTypesController.php +++ b/src/controllers/EntryTypesController.php @@ -20,6 +20,7 @@ use craft\models\Section; use craft\web\Controller; use yii\web\BadRequestHttpException; +use yii\web\ForbiddenHttpException; use yii\web\NotFoundHttpException; use yii\web\Response; @@ -33,13 +34,26 @@ */ class EntryTypesController extends Controller { + private bool $readOnly; + /** * @inheritdoc */ public function beforeAction($action): bool { - // All section actions require an admin - $this->requireAdmin(); + // All actions require an admin account (but not allowAdminChanges) + $this->requireAdmin(false); + + $viewActions = ['edit', 'table-data']; + if (in_array($action->id, $viewActions)) { + // Some actions require admin but not allowAdminChanges + $this->requireAdmin(false); + } else { + // All other actions require an admin & allowAdminChanges + $this->requireAdmin(); + } + + $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; return parent::beforeAction($action); } @@ -54,6 +68,10 @@ public function beforeAction($action): bool */ public function actionEdit(?int $entryTypeId = null, ?EntryType $entryType = null): Response { + if ($entryTypeId === null && $this->readOnly) { + throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); + } + if ($entryTypeId !== null) { if ($entryType === null) { $entryType = Craft::$app->getEntries()->getEntryTypeById($entryTypeId); @@ -91,26 +109,36 @@ public function actionEdit(?int $entryTypeId = null, ?EntryType $entryType = nul ->title($title) ->addCrumb(Craft::t('app', 'Settings'), 'settings') ->addCrumb(Craft::t('app', 'Entry Types'), 'settings/entry-types') - ->action('entry-types/save') - ->redirectUrl('settings/entry-types') - ->addAltAction(Craft::t('app', 'Save and continue editing'), [ - 'redirect' => 'settings/entry-types/{id}', - 'shortcut' => true, - 'retainScroll' => true, - ]) ->contentTemplate('settings/entry-types/_edit.twig', [ 'entryTypeId' => $entryTypeId, 'entryType' => $entryType, 'typeName' => Entry::displayName(), 'lowerTypeName' => Entry::lowerDisplayName(), + 'readOnly' => $this->readOnly, ]); - if ($entryType->id) { + if (!$this->readOnly) { $response - ->addAltAction(Craft::t('app', 'Delete'), [ + ->action('entry-types/save') + ->redirectUrl('settings/entry-types') + ->addAltAction(Craft::t('app', 'Save and continue editing'), [ + 'redirect' => 'settings/entry-types/{id}', + 'shortcut' => true, + 'retainScroll' => true, + ]); + } else { + $response->noticeHtml(Cp::allowAdminChangesReadOnlyNotice()); + } + + if ($entryType->id) { + if (!$this->readOnly) { + $response->addAltAction(Craft::t('app', 'Delete'), [ 'action' => 'entry-types/delete', 'destructive' => true, - ]) + ]); + } + + $response ->metaSidebarHtml(Cp::metadataHtml([ Craft::t('app', 'ID') => $entryType->id, Craft::t('app', 'Used by') => function() use ($entryType) { diff --git a/src/controllers/FieldsController.php b/src/controllers/FieldsController.php index 21c9d46434d..ca30d6b7f25 100644 --- a/src/controllers/FieldsController.php +++ b/src/controllers/FieldsController.php @@ -32,6 +32,7 @@ use craft\web\assets\fieldsettings\FieldSettingsAsset; use craft\web\Controller; use yii\web\BadRequestHttpException; +use yii\web\ForbiddenHttpException; use yii\web\NotFoundHttpException; use yii\web\Response; use yii\web\ServerErrorHttpException; @@ -45,6 +46,8 @@ */ class FieldsController extends Controller { + private bool $readOnly; + /** * @inheritdoc */ @@ -54,8 +57,16 @@ public function beforeAction($action): bool return false; } - // All field actions require an admin - $this->requireAdmin(); + $viewActions = ['edit-field', 'table-data']; + if (in_array($action->id, $viewActions)) { + // Some actions require admin but not allowAdminChanges + $this->requireAdmin(false); + } else { + // All other actions require an admin & allowAdminChanges + $this->requireAdmin(); + } + + $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; return true; } @@ -73,7 +84,9 @@ public function beforeAction($action): bool */ public function actionEditField(?int $fieldId = null, ?FieldInterface $field = null, ?string $type = null): Response { - $this->requireAdmin(); + if ($fieldId === null && $this->readOnly) { + throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); + } $fieldsService = Craft::$app->getFields(); @@ -176,139 +189,151 @@ public function actionEditField(?int $fieldId = null, ?FieldInterface $field = n ->title($title) ->addCrumb(Craft::t('app', 'Settings'), 'settings') ->addCrumb(Craft::t('app', 'Fields'), 'settings/fields') - ->action('fields/save-field') - ->redirectUrl('settings/fields') - ->addAltAction(Craft::t('app', 'Save and continue editing'), [ - 'redirect' => 'settings/fields/edit/{id}', - 'shortcut' => true, - 'retainScroll' => true, - ]) - ->addAltAction(Craft::t('app', 'Save and add another'), [ - 'shortcut' => true, - 'shift' => true, - 'params' => ['addAnother' => 1], - ]) - ->editUrl($field->id ? "settings/fields/edit/$field->id" : null) - ->contentTemplate('settings/fields/_edit.twig', compact( - 'fieldId', - 'field', - 'fieldTypeOptions', - 'missingFieldPlaceholder', - 'supportedTranslationMethods', - )) - ->prepareScreen(function() { - $view = Craft::$app->getView(); - $view->registerAssetBundle(FieldSettingsAsset::class); - $view->registerJsWithVars(fn($typeId, $settingsId, $namespace) => <<contentTemplate('settings/fields/_edit.twig', [ + 'fieldId' => $fieldId, + 'field' => $field, + 'fieldTypeOptions' => $fieldTypeOptions, + 'missingFieldPlaceholder' => $missingFieldPlaceholder, + 'supportedTranslationMethods' => $supportedTranslationMethods, + 'readOnly' => $this->readOnly, + ]); + + if (!$this->readOnly) { + $response + ->action('fields/save-field') + ->redirectUrl('settings/fields') + ->addAltAction(Craft::t('app', 'Save and continue editing'), [ + 'redirect' => 'settings/fields/edit/{id}', + 'shortcut' => true, + 'retainScroll' => true, + ]) + ->addAltAction(Craft::t('app', 'Save and add another'), [ + 'shortcut' => true, + 'shift' => true, + 'params' => ['addAnother' => 1], + ]) + ->editUrl($field->id ? "settings/fields/edit/$field->id" : null); + } else { + $response->noticeHtml(Cp::allowAdminChangesReadOnlyNotice()); + } + + $response + ->prepareScreen(function() { + $view = Craft::$app->getView(); + $view->registerAssetBundle(FieldSettingsAsset::class); + $view->registerJsWithVars(fn($typeId, $settingsId, $namespace) => <<namespaceInputId('type'), - $view->namespaceInputId('settings'), - $view->namespaceInputName('types[__TYPE__]'), - ]); - }); + $view->namespaceInputId('type'), + $view->namespaceInputId('settings'), + $view->namespaceInputName('types[__TYPE__]'), + ]); + }); if ($field->id) { + if (!$this->readOnly) { + $response + ->addAltAction(Craft::t('app', 'Delete'), [ + 'action' => 'fields/delete-field', + 'redirect' => 'settings/fields', + 'destructive' => true, + 'confirm' => Craft::t('app', 'Are you sure you want to delete “{name}”?', [ + 'name' => $field->name, + ]), + ]); + } $response - ->addAltAction(Craft::t('app', 'Delete'), [ - 'action' => 'fields/delete-field', - 'redirect' => 'settings/fields', - 'destructive' => true, - 'confirm' => Craft::t('app', 'Are you sure you want to delete “{name}”?', [ - 'name' => $field->name, - ]), - ]) ->metaSidebarHtml(Cp::metadataHtml([ - Craft::t('app', 'ID') => $field->id, - Craft::t('app', 'Used by') => function() use ($fieldsService, $field) { - $layouts = $fieldsService->findFieldUsages($field); - if (empty($layouts)) { - return Html::tag('i', Craft::t('app', 'No usages')); - } - - /** @var FieldLayout[][] $layoutsByType */ - $layoutsByType = ArrayHelper::index($layouts, - fn(FieldLayout $layout) => $layout->uid, - [fn(FieldLayout $layout) => $layout->type ?? '__UNKNOWN__'], - ); - /** @var FieldLayout[] $unknownLayouts */ - $unknownLayouts = ArrayHelper::remove($layoutsByType, '__UNKNOWN__'); - /** @var FieldLayout[] $layoutsWithProviders */ - $layoutsWithProviders = []; - - // re-fetch as many of these as we can from the element types, - // so they have a chance to supply the layout providers - foreach ($layoutsByType as $type => &$typeLayouts) { - /** @var class-string $type */ - /** @phpstan-ignore-next-line */ - foreach ($type::fieldLayouts(null) as $layout) { - if (isset($typeLayouts[$layout->uid]) && $layout->provider instanceof Chippable) { - $layoutsWithProviders[] = $layout; - unset($typeLayouts[$layout->uid]); - } + Craft::t('app', 'ID') => $field->id, + Craft::t('app', 'Used by') => function() use ($fieldsService, $field) { + $layouts = $fieldsService->findFieldUsages($field); + if (empty($layouts)) { + return Html::tag('i', Craft::t('app', 'No usages')); + } + + /** @var FieldLayout[][] $layoutsByType */ + $layoutsByType = ArrayHelper::index($layouts, + fn(FieldLayout $layout) => $layout->uid, + [fn(FieldLayout $layout) => $layout->type ?? '__UNKNOWN__'], + ); + /** @var FieldLayout[] $unknownLayouts */ + $unknownLayouts = ArrayHelper::remove($layoutsByType, '__UNKNOWN__'); + /** @var FieldLayout[] $layoutsWithProviders */ + $layoutsWithProviders = []; + + // re-fetch as many of these as we can from the element types, + // so they have a chance to supply the layout providers + foreach ($layoutsByType as $type => &$typeLayouts) { + /** @var class-string $type */ + /** @phpstan-ignore-next-line */ + foreach ($type::fieldLayouts(null) as $layout) { + if (isset($typeLayouts[$layout->uid]) && $layout->provider instanceof Chippable) { + $layoutsWithProviders[] = $layout; + unset($typeLayouts[$layout->uid]); } } - unset($typeLayouts); - - $labels = []; - $items = array_map(function(FieldLayout $layout) use (&$labels) { - /** @var FieldLayoutProviderInterface&Chippable $provider */ - $provider = $layout->provider; - $label = $labels[] = $provider->getUiLabel(); - // special case for global sets, where we should link to the settings rather than the edit page - if ($provider instanceof GlobalSet) { - $url = "settings/globals/$provider->id"; - } else { - $url = $provider instanceof CpEditable ? $provider->getCpEditUrl() : null; - } - $icon = $provider instanceof Iconic ? $provider->getIcon() : null; + } + unset($typeLayouts); + + $labels = []; + $items = array_map(function(FieldLayout $layout) use (&$labels) { + /** @var FieldLayoutProviderInterface&Chippable $provider */ + $provider = $layout->provider; + $label = $labels[] = $provider->getUiLabel(); + // special case for global sets, where we should link to the settings rather than the edit page + if ($provider instanceof GlobalSet) { + $url = "settings/globals/$provider->id"; + } else { + $url = $provider instanceof CpEditable ? $provider->getCpEditUrl() : null; + } + $icon = $provider instanceof Iconic ? $provider->getIcon() : null; - $labelHtml = Html::beginTag('span', [ - 'class' => ['flex', 'flex-nowrap', 'gap-s'], + $labelHtml = Html::beginTag('span', [ + 'class' => ['flex', 'flex-nowrap', 'gap-s'], + ]); + if ($icon) { + $labelHtml .= Html::tag('div', Cp::iconSvg($icon), [ + 'class' => array_filter([ + 'cp-icon', + 'small', + $provider instanceof Colorable ? $provider->getColor()?->value : null, + ]), ]); - if ($icon) { - $labelHtml .= Html::tag('div', Cp::iconSvg($icon), [ - 'class' => array_filter([ - 'cp-icon', - 'small', - $provider instanceof Colorable ? $provider->getColor()?->value : null, - ]), - ]); - } - $labelHtml .= Html::tag('span', Html::encode($label)) . - Html::endTag('span'); - - return $url ? Html::a($labelHtml, $url) : $labelHtml; - }, $layoutsWithProviders); - - // sort by label - array_multisort($labels, SORT_ASC, $items); - - foreach ($layoutsByType as $type => $typeLayouts) { - // any remaining layouts for this type? - if (!empty($typeLayouts)) { - /** @var class-string $type */ - $items[] = Craft::t('app', '{total, number} {type} {total, plural, =1{field layout} other{field layouts}}', [ - 'total' => count($typeLayouts), - 'type' => $type::lowerDisplayName(), - ]); - } } + $labelHtml .= Html::tag('span', Html::encode($label)) . + Html::endTag('span'); + + return $url ? Html::a($labelHtml, $url) : $labelHtml; + }, $layoutsWithProviders); + + // sort by label + array_multisort($labels, SORT_ASC, $items); - if (!empty($unknownLayouts)) { + foreach ($layoutsByType as $type => $typeLayouts) { + // any remaining layouts for this type? + if (!empty($typeLayouts)) { + /** @var class-string $type */ $items[] = Craft::t('app', '{total, number} {type} {total, plural, =1{field layout} other{field layouts}}', [ - 'total' => count($unknownLayouts), - 'type' => Craft::t('app', 'unknown'), + 'total' => count($typeLayouts), + 'type' => $type::lowerDisplayName(), ]); } + } - return Html::ul($items, [ - 'encode' => false, + if (!empty($unknownLayouts)) { + $items[] = Craft::t('app', '{total, number} {type} {total, plural, =1{field layout} other{field layouts}}', [ + 'total' => count($unknownLayouts), + 'type' => Craft::t('app', 'unknown'), ]); - }, - ])); + } + + return Html::ul($items, [ + 'encode' => false, + ]); + }, + ])); } return $response; diff --git a/src/controllers/SectionsController.php b/src/controllers/SectionsController.php index a0f13580d23..c88890e2784 100644 --- a/src/controllers/SectionsController.php +++ b/src/controllers/SectionsController.php @@ -15,6 +15,7 @@ use craft\web\assets\editsection\EditSectionAsset; use craft\web\Controller; use yii\web\BadRequestHttpException; +use yii\web\ForbiddenHttpException; use yii\web\NotFoundHttpException; use yii\web\Response; @@ -28,6 +29,8 @@ */ class SectionsController extends Controller { + private bool $readOnly; + /** * @inheritdoc */ @@ -37,8 +40,16 @@ public function beforeAction($action): bool return false; } - // All section actions require an admin - $this->requireAdmin(); + $viewActions = ['index', 'edit-section', 'table-data']; + if (in_array($action->id, $viewActions)) { + // Some actions require admin but not allowAdminChanges + $this->requireAdmin(false); + } else { + // All other actions require an admin & allowAdminChanges + $this->requireAdmin(); + } + + $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; return true; } @@ -52,6 +63,7 @@ public function beforeAction($action): bool public function actionIndex(array $variables = []): Response { $variables['sections'] = Craft::$app->getEntries()->getAllSections(); + $variables['readOnly'] = $this->readOnly; return $this->renderTemplate('settings/sections/_index.twig', $variables); } @@ -67,6 +79,10 @@ public function actionIndex(array $variables = []): Response */ public function actionEditSection(?int $sectionId = null, ?Section $section = null): Response { + if ($sectionId === null && $this->readOnly) { + throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); + } + $sectionsService = Craft::$app->getEntries(); $variables = [ @@ -105,6 +121,7 @@ public function actionEditSection(?int $sectionId = null, ?Section $section = nu $variables['section'] = $section; $variables['typeOptions'] = $typeOptions; + $variables['readOnly'] = $this->readOnly; $this->getView()->registerAssetBundle(EditSectionAsset::class); diff --git a/src/controllers/SystemSettingsController.php b/src/controllers/SystemSettingsController.php index 8738e2951bb..1763ac43960 100644 --- a/src/controllers/SystemSettingsController.php +++ b/src/controllers/SystemSettingsController.php @@ -25,6 +25,7 @@ use craft\web\assets\generalsettings\GeneralSettingsAsset; use craft\web\Controller; use yii\base\Exception; +use yii\web\ForbiddenHttpException; use yii\web\NotFoundHttpException; use yii\web\Response; @@ -289,6 +290,10 @@ public function actionGlobalSetIndex(): Response */ public function actionEditGlobalSet(?int $globalSetId = null, ?GlobalSet $globalSet = null): Response { + if ($globalSetId === null && $this->readOnly) { + throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); + } + if ($globalSet === null) { if ($globalSetId !== null) { $globalSet = Craft::$app->getGlobals()->getSetById($globalSetId); diff --git a/src/controllers/TagsController.php b/src/controllers/TagsController.php index ce676d064f3..d3d09b0acfb 100644 --- a/src/controllers/TagsController.php +++ b/src/controllers/TagsController.php @@ -16,6 +16,7 @@ use craft\models\TagGroup; use craft\web\Controller; use yii\web\BadRequestHttpException; +use yii\web\ForbiddenHttpException; use yii\web\NotFoundHttpException; use yii\web\Response; @@ -36,12 +37,13 @@ class TagsController extends Controller */ public function actionIndex(): Response { - $this->requireAdmin(); + $this->requireAdmin(false); $tagGroups = Craft::$app->getTags()->getAllTagGroups(); return $this->renderTemplate('settings/tags/index.twig', [ 'tagGroups' => $tagGroups, + 'readOnly' => !Craft::$app->getConfig()->getGeneral()->allowAdminChanges, ]); } @@ -55,7 +57,14 @@ public function actionIndex(): Response */ public function actionEditTagGroup(?int $tagGroupId = null, ?TagGroup $tagGroup = null): Response { - $this->requireAdmin(); + $this->requireAdmin(false); + + $readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; + + if ($tagGroupId === null && $readOnly) { + throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); + } + if ($tagGroupId !== null) { if ($tagGroup === null) { @@ -92,6 +101,7 @@ public function actionEditTagGroup(?int $tagGroupId = null, ?TagGroup $tagGroup 'tagGroup' => $tagGroup, 'title' => $title, 'crumbs' => $crumbs, + 'readOnly' => $readOnly, ]); } diff --git a/src/helpers/Cp.php b/src/helpers/Cp.php index 172af5ac1c0..b67d873d359 100644 --- a/src/helpers/Cp.php +++ b/src/helpers/Cp.php @@ -2476,6 +2476,8 @@ public static function cardViewDesignerHtml(FieldLayout $fieldLayout, array $con 'id' => 'cvd' . mt_rand(), ]; + $disabled = isset($config['config']['disabled']) && $config['config']['disabled']; + // get the attributes that are set to be visible in the card body $selectedCardAttributes = $fieldLayout->getCardBodyAttributes(); @@ -2543,6 +2545,7 @@ public static function cardViewDesignerHtml(FieldLayout $fieldLayout, array $con 'required' => true, //'targetPrefix' => 'cardView-', 'sortable' => true, + 'disabled' => $disabled, ]); @@ -2671,6 +2674,7 @@ public static function fieldLayoutDesignerHtml(FieldLayout $fieldLayout, array $ 'customizableUi' => true, ]; + $disabled = isset($config['disabled']) && $config['disabled']; $tabs = array_values($fieldLayout->getTabs()); if (!$config['customizableTabs']) { @@ -2749,7 +2753,7 @@ public static function fieldLayoutDesignerHtml(FieldLayout $fieldLayout, array $ return Html::beginTag('div', [ 'id' => $config['id'], - 'class' => 'layoutdesigner', + 'class' => 'layoutdesigner' . ($disabled ? ' disabled' : ''), ]) . Html::hiddenInput('fieldLayout', Json::encode($fieldLayoutConfig), [ 'data' => ['config-input' => true], @@ -3405,4 +3409,29 @@ public static function requestedSite(): ?Site return self::$_requestedSite ?: null; } + + /** + * Returns the notice that should show when admin is viewing the available settings pages + * while `allowAdminChanges` is set to false. + * + * @return string + * @since 5.6.0 + */ + public static function allowAdminChangesReadOnlyNotice(): string + { + return + Html::beginTag('div', [ + 'class' => 'content-notice', + ]) . + Html::tag('div', '', [ + 'class' => ['content-notice-icon'], + 'aria' => ['hidden' => 'true'], + 'data' => ['icon' => 'lightbulb'], + ]) . + Html::tag('p', Craft::t( + 'app', + '`allowAdminChanges` is off. You can view the settings, but not change them.', + )) . + Html::endTag('div'); + } } diff --git a/src/templates/_includes/forms/autosuggest.twig b/src/templates/_includes/forms/autosuggest.twig index 320c8fe64f5..ecbf361ba08 100644 --- a/src/templates/_includes/forms/autosuggest.twig +++ b/src/templates/_includes/forms/autosuggest.twig @@ -14,7 +14,6 @@ {%- set class = (class ?? [])|explodeClass|merge([ 'text', - (disabled ?? false) ? 'disabled' : null, not (size ?? false) ? 'fullwidth' : null, ]|filter) %} diff --git a/src/templates/_includes/forms/checkboxSelect.twig b/src/templates/_includes/forms/checkboxSelect.twig index 3a028f0a866..bfa632ad0b2 100644 --- a/src/templates/_includes/forms/checkboxSelect.twig +++ b/src/templates/_includes/forms/checkboxSelect.twig @@ -1,6 +1,7 @@ {%- set id = id ?? "checkbox-select-#{random()}" %} {%- set options = options ?? [] %} {%- set values = values ?? [] %} +{%- set disabled = disabled ?? false %} {%- set sortable = sortable ?? false %} {%- set showAllOption = showAllOption ?? false %} @@ -44,7 +45,7 @@ {% if not showAllOption or option.value is not defined or option.value != allValue %}
{% set selected = (showAllOption and allChecked) or (option.value is defined and option.value in values) %} - {% if sortable %} + {% if sortable and not disabled %} {{ tag('a', { class: { move: true, @@ -65,7 +66,7 @@ {% endfor %} {% endtag %} -{% if sortable %} +{% if sortable and not disabled %} {% js %} new Craft.SortableCheckboxSelect($('#{{ id|namespaceInputId }}')); {% endjs %} diff --git a/src/templates/settings/categories/_edit.twig b/src/templates/settings/categories/_edit.twig index 543504ed209..db884776d04 100644 --- a/src/templates/settings/categories/_edit.twig +++ b/src/templates/settings/categories/_edit.twig @@ -1,6 +1,9 @@ {% extends '_layouts/cp.twig' %} -{% set fullPageForm = true %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} +{% set fullPageForm = not readOnly %} {% set formActions = [ { @@ -15,12 +18,17 @@ {% set headlessMode = craft.app.config.general.headlessMode %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} {% block content %} - {{ actionInput('categories/save-group') }} - {{ redirectInput('settings/categories') }} + {% if not readOnly %} + {{ actionInput('categories/save-group') }} + {{ redirectInput('settings/categories') }} - {% if categoryGroup.id %}{{ hiddenInput('groupId', categoryGroup.id) }}{% endif %} + {% if categoryGroup.id %}{{ hiddenInput('groupId', categoryGroup.id) }}{% endif %} + {% endif %} {{ forms.textField({ first: true, @@ -32,6 +40,7 @@ errors: categoryGroup.getErrors('name'), autofocus: true, required: true, + disabled: readOnly, }) }} {{ forms.textField({ @@ -44,7 +53,8 @@ autocapitalize: false, value: categoryGroup.handle, errors: categoryGroup.getErrors('handle'), - required: true + required: true, + disabled: readOnly, }) }} {{ forms.textField({ @@ -54,7 +64,8 @@ name: 'maxLevels', value: categoryGroup.maxLevels, size: 5, - errors: categoryGroup.getErrors('maxLevels') + errors: categoryGroup.getErrors('maxLevels'), + disabled: readOnly, }) }} {{ forms.selectField({ @@ -67,6 +78,7 @@ {label: 'After other {type}'|t('app', {type: 'categories'|t('app')}), value: 'end'}, ], value: categoryGroup.defaultPlacement, + disabled: readOnly, }) }}
@@ -125,7 +137,8 @@ allowAdd: false, allowDelete: false, allowReorder: false, - errors: siteErrors|unique + errors: siteErrors|unique, + static: readOnly, }) }}
@@ -133,6 +146,7 @@ {{ forms.fieldLayoutDesignerField({ fieldLayout: categoryGroup.getFieldLayout(), withCardViewDesigner: true, + disabled: readOnly, }) }} {% endblock %} diff --git a/src/templates/settings/categories/index.twig b/src/templates/settings/categories/index.twig index 507ff20f9a9..501e5934185 100644 --- a/src/templates/settings/categories/index.twig +++ b/src/templates/settings/categories/index.twig @@ -1,5 +1,8 @@ {% extends "_layouts/cp" %} {% set title = "Category Groups"|t('app') %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} @@ -11,14 +14,20 @@ ]) %} {% block actionButton %} - {% set buttonLabel = "New category group"|t('app') %} - {{ buttonLabel }} + {% if not readOnly %} + {% set buttonLabel = "New category group"|t('app') %} + {{ buttonLabel }} + {% endif %} {% endblock %} {% set crumbs = [ { label: "Settings"|t('app'), url: url('settings') } ] %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} + {% block content %}
{% endblock %} @@ -54,11 +63,16 @@ }, ]; - new Craft.VueAdminTable({ + let config = { columns: columns, container: '#categorygroups-vue-admin-table', - deleteAction: 'categories/delete-category-group', emptyMessage: Craft.t('app', 'No category groups exist yet.'), tableData: {{ tableData|json_encode|raw }} - }); + }; + + if (!{{ readOnly }}) { + config['deleteAction'] = 'categories/delete-category-group'; + } + + new Craft.VueAdminTable(config); {% endjs %} diff --git a/src/templates/settings/entry-types/_edit.twig b/src/templates/settings/entry-types/_edit.twig index 784c03e24b5..874c7ecde65 100644 --- a/src/templates/settings/entry-types/_edit.twig +++ b/src/templates/settings/entry-types/_edit.twig @@ -13,6 +13,7 @@ required: true, errors: entryType.getErrors('name'), data: {'error-key': 'name'}, + disabled: readOnly, }) }} {{ forms.textField({ @@ -27,6 +28,7 @@ required: true, errors: entryType.getErrors('handle'), data: {'error-key': 'handle'}, + disabled: readOnly, }) }} {{ forms.iconPickerField({ @@ -38,6 +40,7 @@ data: { 'error-key': 'icon', }, + static: readOnly, }) }} {{ forms.colorSelectField({ @@ -49,6 +52,7 @@ data: { 'error-key': 'color', }, + disabled: readOnly, }) }}
@@ -73,7 +77,8 @@ ]|filter, value: entryType.titleTranslationMethod, toggle: true, - targetPrefix: 'translation-method-' + targetPrefix: 'translation-method-', + disabled: readOnly, }) }}
@@ -91,6 +96,7 @@ data: { 'error-key': 'titleTranslationKeyFormat', }, + disabled: readOnly, }) }}
@@ -107,6 +113,7 @@ data: { 'error-key': 'titleFormat', }, + disabled: readOnly, }) }} {{ forms.lightswitchField({ @@ -114,7 +121,8 @@ name: 'showSlugField', toggle: 'slug-container', reverseToggle: '#slugFormat-container, #field-layout .fld-slug-field-icon', - on: entryType.showSlugField + on: entryType.showSlugField, + disabled: readOnly, }) }} {% if craft.app.getIsMultiSite() %} @@ -137,7 +145,8 @@ ]|filter, value: entryType.slugTranslationMethod, toggle: true, - targetPrefix: 'slug-translation-method-' + targetPrefix: 'slug-translation-method-', + disabled: readOnly, }) }}
@@ -155,6 +164,7 @@ data: { 'error-key': 'slugTranslationKeyFormat', }, + disabled: readOnly, }) }}
@@ -169,6 +179,7 @@ data: { 'error-key': 'showStatusField', }, + disabled: readOnly, }) }}
@@ -177,6 +188,7 @@ id: 'field-layout', fieldLayout: entryType.getFieldLayout(), withCardViewDesigner: true, + disabled: readOnly, }) }} {% if not entryType.handle %} diff --git a/src/templates/settings/entry-types/index.twig b/src/templates/settings/entry-types/index.twig index cca9a5f51cd..b65c1f7e97a 100644 --- a/src/templates/settings/entry-types/index.twig +++ b/src/templates/settings/entry-types/index.twig @@ -1,6 +1,10 @@ {% extends '_layouts/cp' %} {% set selectedTab = 'entryTypes' %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} + {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} {% do view.registerTranslations('app', [ @@ -23,9 +27,15 @@ ] %} {% block actionButton %} - {{ "New entry type"|t('app') }} + {% if not readOnly %} + {{ "New entry type"|t('app') }} + {% endif %} {% endblock %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} + {% block content %}
{% endblock %} @@ -42,14 +52,19 @@ }, ]; - new Craft.VueAdminTable({ - columns, - container: '#entrytypes-vue-admin-table', - deleteAction: 'entry-types/delete', - deleteConfirmationMessage: Craft.t('app', 'Are you sure you want to delete “{name}” and all entries of that type?'), - emptyMessage: Craft.t('app', 'No entry types exist yet.'), - tableDataEndpoint: 'entry-types/table-data', - search: true, - }); + let config = { + columns, + container: '#entrytypes-vue-admin-table', + emptyMessage: Craft.t('app', 'No entry types exist yet.'), + tableDataEndpoint: 'entry-types/table-data', + search: true, + } + + if (!{{ readOnly }}) { + config['deleteAction'] = 'entry-types/delete'; + config['deleteConfirmationMessage'] = Craft.t('app', 'Are you sure you want to delete “{name}” and all entries of that type?'); + } + + new Craft.VueAdminTable(config); })(); {% endjs %} diff --git a/src/templates/settings/fields/_edit.twig b/src/templates/settings/fields/_edit.twig index 40e858fcad4..72e2e3af2fb 100644 --- a/src/templates/settings/fields/_edit.twig +++ b/src/templates/settings/fields/_edit.twig @@ -1,6 +1,10 @@ {% import '_includes/forms.twig' as forms %} -{% if fieldId is defined and fieldId %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} + +{% if not readOnly and fieldId is defined and fieldId %} {{ hiddenInput('fieldId', fieldId) }} {% endif %} @@ -15,6 +19,7 @@ autofocus: true, errors: field.getErrors('name'), data: {'error-key': 'name'}, + disabled: readOnly, }) }} {{ forms.textField({ @@ -30,6 +35,7 @@ required: true, errors: field.getErrors('handle'), data: {'error-key': 'handle'}, + disabled: readOnly, }) }} {{ forms.textareaField({ @@ -43,13 +49,15 @@ data: { 'error-key': 'instructions', }, + disabled: readOnly, }) }} {{ forms.lightswitchField({ label: 'Use this field’s values as search keywords'|t('app'), id: 'searchable', name: 'searchable', - on: field.searchable + on: field.searchable, + disabled: readOnly, }) }} {{ forms.customSelectField({ @@ -64,6 +72,7 @@ data: { 'error-key': 'type', }, + disabled: readOnly, }) }} {{ missingFieldPlaceholder|raw }} @@ -86,7 +95,8 @@ ]|filter, value: field.translationMethod, toggle: true, - targetPrefix: 'translation-method-' + targetPrefix: 'translation-method-', + disabled: readOnly, }) }} {% if 'custom' in translationMethods %} @@ -103,6 +113,7 @@ value: field.translationKeyFormat, errors: field.getErrors('translationKeyFormat'), data: {'error-key': 'translationKeyFormat'}, + disabled: readOnly, }) }} {% endtag %} {% endif %} @@ -112,7 +123,7 @@
-
+
{% include 'settings/fields/_type-settings' with { namespace: 'types['~className(field)|id~']' diff --git a/src/templates/settings/fields/index.twig b/src/templates/settings/fields/index.twig index 202228c8b0a..5e1209fd063 100644 --- a/src/templates/settings/fields/index.twig +++ b/src/templates/settings/fields/index.twig @@ -1,8 +1,12 @@ -{% requireAdmin %} +{% requireAdmin '0' %} {% extends "_layouts/cp" %} {% set title = "Fields"|t('app') %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} + {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} {% do view.registerTranslations('app', [ @@ -24,10 +28,15 @@ {% block actionButton %} - {% set newFieldUrl = url('settings/fields/new') %} - {{ "New field"|t('app') }} + {% if not readOnly %} + {% set newFieldUrl = url('settings/fields/new') %} + {{ "New field"|t('app') }} + {% endif %} {% endblock %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} {% block content %}
@@ -91,14 +100,19 @@ callback: (value) => value || `${Craft.t('app', 'No usages')}`, }); - new Craft.VueAdminTable({ + let config = { columns, container: '#fields-vue-admin-table', - deleteAction: 'fields/delete-field', emptyMessage: info.emptyMessage, tableDataEndpoint: 'fields/table-data', search: true, - }); + } + + if (!{{ readOnly }}) { + config['deleteAction'] = 'fields/delete-field'; + } + + new Craft.VueAdminTable(config); })({{ { isMultiSite: craft.app.isMultiSite, diff --git a/src/templates/settings/globals/_edit.twig b/src/templates/settings/globals/_edit.twig index 2bac59cc584..0b9d9bdb13b 100644 --- a/src/templates/settings/globals/_edit.twig +++ b/src/templates/settings/globals/_edit.twig @@ -1,5 +1,10 @@ {% extends '_layouts/cp.twig' %} -{% set fullPageForm = true %} + +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} + +{% set fullPageForm = not readOnly %} {% set formActions = [ { @@ -14,10 +19,12 @@ {% block content %} - {{ actionInput('globals/save-set') }} - {{ redirectInput('settings/globals') }} + {% if not readOnly %} + {{ actionInput('globals/save-set') }} + {{ redirectInput('settings/globals') }} - {% if globalSet.id %}{{ hiddenInput('setId', globalSet.id) }}{% endif %} + {% if globalSet.id %}{{ hiddenInput('setId', globalSet.id) }}{% endif %} + {% endif %} {{ forms.textField({ first: true, @@ -29,6 +36,7 @@ errors: globalSet.getErrors('name'), autofocus: true, required: true, + disabled: readOnly, }) }} {{ forms.textField({ @@ -41,13 +49,15 @@ autocapitalize: false, value: globalSet.handle, errors: globalSet.getErrors('handle'), - required: true + required: true, + disabled: readOnly, }) }}
{{ forms.fieldLayoutDesignerField({ fieldLayout: globalSet.getFieldLayout(), + disabled: readOnly, }) }} {% endblock %} diff --git a/src/templates/settings/globals/_index.twig b/src/templates/settings/globals/_index.twig index ca3ac6605c0..0acef268950 100644 --- a/src/templates/settings/globals/_index.twig +++ b/src/templates/settings/globals/_index.twig @@ -1,13 +1,23 @@ -{% requireAdmin %} +{% requireAdmin '0' %} {% extends "_layouts/cp" %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} + {% block actionButton %} - - {{ buttonLabel }} - + {% if not readOnly %} + + {{ buttonLabel }} + + {% endif %} {% endblock %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} + {% block content %}
{% endblock %} @@ -34,12 +44,17 @@ var columns = [ }, ]; -new Craft.VueAdminTable({ +let config = { columns: columns, container: '#sets-vue-admin-table', - deleteAction: 'globals/delete-set', emptyMessage: Craft.t('app', 'No global sets exist yet.'), - reorderAction: '{{ globalSets|length > 1 ? 'globals/reorder-sets' : ''}}', tableData: {{ tableData|json_encode|raw }} -}); +} + +if (!{{ readOnly }}) { + config['deleteAction'] = 'globals/delete-set'; + config['reorderAction'] = '{{ globalSets|length > 1 ? 'globals/reorder-sets' : ''}}'; +} + +new Craft.VueAdminTable(config); {% endjs %} diff --git a/src/templates/settings/sections/_edit.twig b/src/templates/settings/sections/_edit.twig index 3793331a077..9b7a82daaf6 100644 --- a/src/templates/settings/sections/_edit.twig +++ b/src/templates/settings/sections/_edit.twig @@ -1,5 +1,9 @@ {% extends "_layouts/cp" %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} + {% set crumbs = [ { label: 'Settings'|t('app'), @@ -12,7 +16,7 @@ ] %} {% set selectedTab = 'settings' %} -{% set fullPageForm = true %} +{% set fullPageForm = not readOnly %} {% set formActions = [ { label: 'Save and continue editing'|t('app'), @@ -26,12 +30,17 @@ {% set headlessMode = craft.app.config.general.headlessMode %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} {% block content %} - {{ actionInput('sections/save-section') }} - {{ redirectInput('settings/sections') }} + {% if not readOnly %} + {{ actionInput('sections/save-section') }} + {{ redirectInput('settings/sections') }} - {% if section.id %}{{ hiddenInput('sectionId', section.id) }}{% endif %} + {% if section.id %}{{ hiddenInput('sectionId', section.id) }}{% endif %} + {% endif %} {{ forms.textField({ first: true, @@ -43,6 +52,7 @@ errors: section.getErrors('name'), autofocus: true, required: true, + disabled: readOnly, }) }} {{ forms.textField({ @@ -55,14 +65,16 @@ autocapitalize: false, value: section.handle, errors: section.getErrors('handle'), - required: true + required: true, + disabled: readOnly, }) }} {{ forms.lightswitchField({ label: 'Enable versioning for entries in this section'|t('app'), id: 'enableVersioning', name: 'enableVersioning', - on: section.enableVersioning + on: section.enableVersioning, + disabled: readOnly, }) }} {{ forms.selectField({ @@ -75,7 +87,8 @@ value: section.type, toggle: true, targetPrefix: '.type-', - errors: section.getErrors('type') + errors: section.getErrors('type'), + disabled: readOnly, }) }}
@@ -87,7 +100,8 @@ name: 'entryTypes[]', values: section.getEntryTypes(), errors: section.getErrors('entryTypes'), - create: true, + create: not readOnly, + disabled: readOnly, }) }} {% set siteRows = [] %} @@ -107,18 +121,19 @@ name: 'sites['~site.handle~'][enabled]', on: brandNewSection or siteSettings, value: site.id, - small: true + small: true, + disabled: readOnly, }), singleHomepage: { - value: (section.type == 'single' and siteSettings and siteSettings.uriFormat == '__home__') + value: (section.type == 'single' and siteSettings and siteSettings.uriFormat == '__home__'), }, singleUri: { - value: (section.type == 'single' and siteSettings and siteSettings.uriFormat != '__home__') ? siteSettings.uriFormat, - hasErrors: (section.type == 'single' and siteSettings ? siteSettings.hasErrors('uriFormat')) + value: (section.type == 'single' and siteSettings and siteSettings.uriFormat != '__home__') ? siteSettings.uriFormat, + hasErrors: (section.type == 'single' and siteSettings ? siteSettings.hasErrors('uriFormat')), }, uriFormat: { value: siteSettings ? siteSettings.uriFormat, - hasErrors: (section.type != 'single' and siteSettings ? siteSettings.hasErrors('uriFormat')) + hasErrors: (section.type != 'single' and siteSettings ? siteSettings.hasErrors('uriFormat')), }, template: not headlessMode ? { value: siteSettings ? siteSettings.template, @@ -190,7 +205,8 @@ allowAdd: false, allowDelete: false, allowReorder: false, - errors: siteErrors|unique + errors: siteErrors|unique, + static: readOnly, }) }} {% if craft.app.getIsMultiSite() %} @@ -208,7 +224,8 @@ { value: 'all', label: 'Save entries to all sites enabled for this section'|t('app') }, { value: 'custom', label: 'Let each entry choose which sites it should be saved to'|t('app') }, ], - value: section.propagationMethod.value + value: section.propagationMethod.value, + disabled: readOnly, }) }}
{% endif %} @@ -221,7 +238,8 @@ name: 'maxLevels', value: section.maxLevels, size: 5, - errors: section.getErrors('maxLevels') + errors: section.getErrors('maxLevels'), + disabled: readOnly, }) }} {{ forms.selectField({ @@ -234,6 +252,7 @@ {label: 'After other {type}'|t('app', {type: 'entries'|t('app')}), value: 'end'}, ], value: section.defaultPlacement, + disabled: readOnly, }) }}
@@ -267,7 +286,8 @@ allowReorder: true, allowDelete: true, rows: section.previewTargets, - errors: section.getErrors('previewTargets') + errors: section.getErrors('previewTargets'), + static: readOnly, }) }} {{ forms.textField({ @@ -281,7 +301,8 @@ value: section.maxAuthors, errors: section.getErrors('maxAuthors'), size: 5, - required: true + required: true, + disabled: readOnly, }) }} {% endblock %} diff --git a/src/templates/settings/sections/_index.twig b/src/templates/settings/sections/_index.twig index 6e59fd7c7ff..52bb9284516 100644 --- a/src/templates/settings/sections/_index.twig +++ b/src/templates/settings/sections/_index.twig @@ -1,6 +1,10 @@ {% extends "_layouts/cp" %} {% set title = "Sections"|t('app') %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} + {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} {% do view.registerTranslations('app', [ @@ -20,9 +24,14 @@ ] %} {% block actionButton %} - {{ "New section"|t('app') }} + {% if not readOnly %} + {{ "New section"|t('app') }} + {% endif %} {% endblock %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} {% block content %}
@@ -36,13 +45,18 @@ { name: 'type', title: Craft.t('app', 'Type'), sortField: true }, ]; - new Craft.VueAdminTable({ + let config = { columns, container: '#sections-vue-admin-table', - deleteAction: 'sections/delete-section', - deleteConfirmationMessage: Craft.t('app', "Are you sure you want to delete “{name}” and all its entries?"), emptyMessage: Craft.t('app', 'No sections exist yet.'), tableDataEndpoint: 'sections/table-data', - }); + }; + + if (!{{ readOnly }}) { + config['deleteAction'] = 'sections/delete-section'; + config['deleteConfirmationMessage'] = Craft.t('app', "Are you sure you want to delete “{name}” and all its entries?"); + } + + new Craft.VueAdminTable(config); })(); {% endjs %} diff --git a/src/templates/settings/tags/_edit.twig b/src/templates/settings/tags/_edit.twig index eabd214c038..d204da6970a 100644 --- a/src/templates/settings/tags/_edit.twig +++ b/src/templates/settings/tags/_edit.twig @@ -1,6 +1,9 @@ {% extends '_layouts/cp.twig' %} -{% set fullPageForm = true %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} +{% set fullPageForm = not readOnly %} {% set formActions = [ { @@ -13,11 +16,17 @@ {% import '_includes/forms.twig' as forms %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} + {% block content %} - {{ actionInput('tags/save-tag-group') }} - {{ redirectInput('settings/tags') }} + {% if not readOnly %} + {{ actionInput('tags/save-tag-group') }} + {{ redirectInput('settings/tags') }} - {% if tagGroup.id %}{{ hiddenInput('tagGroupId', tagGroup.id) }}{% endif %} + {% if tagGroup.id %}{{ hiddenInput('tagGroupId', tagGroup.id) }}{% endif %} + {% endif %} {{ forms.textField({ first: true, @@ -29,6 +38,7 @@ errors: tagGroup.getErrors('name'), autofocus: true, required: true, + disabled: readOnly, }) }} {{ forms.textField({ @@ -41,13 +51,15 @@ autocapitalize: false, value: tagGroup.handle, errors: tagGroup.getErrors('handle'), - required: true + required: true, + disabled: readOnly, }) }}
{{ forms.fieldLayoutDesignerField({ fieldLayout: tagGroup.getFieldLayout(), + disabled: readOnly, }) }} {% endblock %} diff --git a/src/templates/settings/tags/index.twig b/src/templates/settings/tags/index.twig index 3cad6a04f98..9c58a9be5f5 100644 --- a/src/templates/settings/tags/index.twig +++ b/src/templates/settings/tags/index.twig @@ -1,5 +1,8 @@ {% extends "_layouts/cp" %} {% set title = "Tag Groups"|t('app') %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} @@ -10,13 +13,19 @@ ]) %} {% block actionButton %} - {{ "New tag group"|t('app') }} + {% if not readOnly %} + {{ "New tag group"|t('app') }} + {% endif %} {% endblock %} {% set crumbs = [ { label: "Settings"|t('app'), url: url('settings') } ] %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} + {% block content %}
{% endblock %} @@ -38,11 +47,16 @@ { name: '__slot:handle', title: Craft.t('app', 'Handle') }, ]; - new Craft.VueAdminTable({ + let config = { columns: columns, container: '#taggroups-vue-admin-table', - deleteAction: 'tags/delete-tag-group', emptyMessage: Craft.t('app', 'No tag groups exist yet.'), tableData: {{ tableData|json_encode|raw }} - }); + }; + + if (!{{ readOnly }}) { + config['deleteAction'] = 'tags/delete-tag-group'; + } + + new Craft.VueAdminTable(config); {% endjs %} diff --git a/src/web/assets/cp/src/css/_main.scss b/src/web/assets/cp/src/css/_main.scss index b55a41edf3c..9f664751153 100644 --- a/src/web/assets/cp/src/css/_main.scss +++ b/src/web/assets/cp/src/css/_main.scss @@ -2070,7 +2070,7 @@ ul.icons { } .disabled:not(.status, .status-label) { - opacity: 0.25; + opacity: 0.5; pointer-events: none; user-select: none; } diff --git a/src/web/assets/cp/src/js/FieldLayoutDesigner.js b/src/web/assets/cp/src/js/FieldLayoutDesigner.js index 188975c26e2..2d93c26318f 100644 --- a/src/web/assets/cp/src/js/FieldLayoutDesigner.js +++ b/src/web/assets/cp/src/js/FieldLayoutDesigner.js @@ -180,7 +180,7 @@ Craft.FieldLayoutDesigner = Garnish.Base.extend( cvd.$libraryContainer .data('sortableCheckboxSelect') - .dragSort.on('dragStop', function () { + ?.dragSort.on('dragStop', function () { cvd.updatePreview(); }); }, @@ -1883,12 +1883,12 @@ Craft.FieldLayoutDesigner.CardViewDesigner = Garnish.Base.extend({ let cvd = this.$container.data('cvd'); // when item is moved up or down via disclosure menu - update preview - this.sortableCheckboxSelect.$container.on('movedUp movedDown', function () { + this.sortableCheckboxSelect?.$container.on('movedUp movedDown', function () { cvd.updatePreview(); }); // when checkbox is checked or unchecked - apply config & update preview - this.sortableCheckboxSelect.$container.on( + this.sortableCheckboxSelect?.$container.on( 'checked unchecked', function (ev) { let val = $(ev.target).find('input.checkbox').val(); diff --git a/src/web/twig/variables/Cp.php b/src/web/twig/variables/Cp.php index 803a072606b..57b9456814c 100644 --- a/src/web/twig/variables/Cp.php +++ b/src/web/twig/variables/Cp.php @@ -1107,19 +1107,6 @@ public function fieldLayoutDesigner(FieldLayout $fieldLayout, array $config = [] */ public function allowAdminChangesReadOnlyNotice(): string { - return - Html::beginTag('div', [ - 'class' => 'content-notice', - ]) . - Html::tag('div', '', [ - 'class' => ['content-notice-icon'], - 'aria' => ['hidden' => 'true'], - 'data' => ['icon' => 'lightbulb'], - ]) . - Html::tag('p', Craft::t( - 'app', - '`allowAdminChanges` is off. You can view the settings, but not change them.', - )) . - Html::endTag('div'); + return CpHelper::allowAdminChangesReadOnlyNotice(); } } From f3b4dcb708a56b0a7db6752faeaf97890aba12d4 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Tue, 3 Dec 2024 12:41:12 +0000 Subject: [PATCH 06/56] show settings > media items in readOnly mode --- src/controllers/FsController.php | 45 +- src/controllers/ImageTransformsController.php | 21 +- src/controllers/VolumesController.php | 49 ++- src/fs/Local.php | 1 + .../_components/fs/Local/settings.twig | 1 + .../settings/assets/transforms/_index.twig | 26 +- .../settings/assets/transforms/_settings.twig | 411 +++++++++--------- .../settings/assets/volumes/_edit.twig | 19 +- .../settings/assets/volumes/_index.twig | 28 +- src/templates/settings/filesystems/_edit.twig | 28 +- .../settings/filesystems/_index.twig | 22 +- 11 files changed, 403 insertions(+), 248 deletions(-) diff --git a/src/controllers/FsController.php b/src/controllers/FsController.php index 22af0953f24..4e5a0349dc8 100644 --- a/src/controllers/FsController.php +++ b/src/controllers/FsController.php @@ -11,6 +11,7 @@ use craft\base\Fs; use craft\base\FsInterface; use craft\helpers\ArrayHelper; +use craft\helpers\Cp; use craft\helpers\Html; use craft\web\Controller; use yii\web\BadRequestHttpException; @@ -29,6 +30,8 @@ */ class FsController extends Controller { + private bool $readOnly; + /** * @inheritdoc */ @@ -38,8 +41,16 @@ public function beforeAction($action): bool return false; } - // All asset volume actions require an admin - $this->requireAdmin(); + $viewActions = ['index', 'edit']; + if (in_array($action->id, $viewActions)) { + // Some actions require admin but not allowAdminChanges + $this->requireAdmin(false); + } else { + // All other actions require an admin & allowAdminChanges + $this->requireAdmin(); + } + + $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; return true; } @@ -53,6 +64,7 @@ public function actionIndex(): Response { $variables = []; $variables['filesystems'] = Craft::$app->getFs()->getAllFilesystems(); + $variables['readOnly'] = $this->readOnly; return $this->renderTemplate('settings/filesystems/_index.twig', $variables); } @@ -68,7 +80,9 @@ public function actionIndex(): Response */ public function actionEdit(?string $handle = null, ?Fs $filesystem = null): Response { - $this->requireAdmin(); + if ($handle === null && $this->readOnly) { + throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); + } $fsService = Craft::$app->getFs(); @@ -111,24 +125,33 @@ public function actionEdit(?string $handle = null, ?Fs $filesystem = null): Resp $title = Craft::t('app', 'Create a new filesystem'); } - return $this->asCpScreen() + $response = $this->asCpScreen() ->title($title) ->addCrumb(Craft::t('app', 'Settings'), 'settings') ->addCrumb(Craft::t('app', 'Filesystems'), 'settings/filesystems') - ->action('fs/save') - ->redirectUrl('settings/filesystems') - ->addAltAction(Craft::t('app', 'Save and continue editing'), [ - 'redirect' => 'settings/filesystems/{handle}', - 'shortcut' => true, - 'retainScroll' => true, - ]) ->contentTemplate('settings/filesystems/_edit.twig', [ 'oldHandle' => $handle, 'filesystem' => $filesystem, 'fsOptions' => $fsOptions, 'fsInstances' => $fsInstances, 'fsTypes' => $allFsTypes, + 'readOnly' => $this->readOnly, ]); + + if (!$this->readOnly) { + $response + ->action('fs/save') + ->redirectUrl('settings/filesystems') + ->addAltAction(Craft::t('app', 'Save and continue editing'), [ + 'redirect' => 'settings/filesystems/{handle}', + 'shortcut' => true, + 'retainScroll' => true, + ]); + } else { + $response->noticeHtml(Cp::allowAdminChangesReadOnlyNotice()); + } + + return $response; } /** diff --git a/src/controllers/ImageTransformsController.php b/src/controllers/ImageTransformsController.php index 45e581a61ae..fee47c0eb4b 100644 --- a/src/controllers/ImageTransformsController.php +++ b/src/controllers/ImageTransformsController.php @@ -13,6 +13,7 @@ use craft\validators\ColorValidator; use craft\web\assets\edittransform\EditTransformAsset; use craft\web\Controller; +use yii\web\ForbiddenHttpException; use yii\web\NotFoundHttpException; use yii\web\Response; @@ -26,6 +27,8 @@ */ class ImageTransformsController extends Controller { + private bool $readOnly; + /** * @inheritdoc */ @@ -35,8 +38,16 @@ public function beforeAction($action): bool return false; } - // All image transform actions require an admin - $this->requireAdmin(); + $viewActions = ['index', 'edit']; + if (in_array($action->id, $viewActions)) { + // Some actions require admin but not allowAdminChanges + $this->requireAdmin(false); + } else { + // All other actions require an admin & allowAdminChanges + $this->requireAdmin(); + } + + $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; return true; } @@ -52,6 +63,7 @@ public function actionIndex(): Response $variables['transforms'] = Craft::$app->getImageTransforms()->getAllTransforms(); $variables['modes'] = ImageTransform::modes(); + $variables['readOnly'] = $this->readOnly; return $this->renderTemplate('settings/assets/transforms/_index.twig', $variables); } @@ -66,6 +78,10 @@ public function actionIndex(): Response */ public function actionEdit(?string $transformHandle = null, ?ImageTransform $transform = null): Response { + if ($transformHandle === null && $this->readOnly) { + throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); + } + if ($transform === null) { if ($transformHandle !== null) { $transform = Craft::$app->getImageTransforms()->getTransformByHandle($transformHandle); @@ -115,6 +131,7 @@ public function actionEdit(?string $transformHandle = null, ?ImageTransform $tra 'title' => $title, 'qualityPickerOptions' => $qualityPickerOptions, 'qualityPickerValue' => $qualityPickerValue, + 'readOnly' => $this->readOnly, ]); } diff --git a/src/controllers/VolumesController.php b/src/controllers/VolumesController.php index cbd11eca3a3..9f277490d18 100644 --- a/src/controllers/VolumesController.php +++ b/src/controllers/VolumesController.php @@ -12,6 +12,7 @@ use craft\base\FsInterface; use craft\elements\Asset; use craft\helpers\Assets; +use craft\helpers\Cp; use craft\helpers\FileHelper; use craft\helpers\Json; use craft\models\Volume; @@ -32,6 +33,8 @@ */ class VolumesController extends Controller { + private bool $readOnly; + /** * @inheritdoc */ @@ -41,8 +44,16 @@ public function beforeAction($action): bool return false; } - // All asset volume actions require an admin - $this->requireAdmin(); + $viewActions = ['volume-index', 'edit-volume']; + if (in_array($action->id, $viewActions)) { + // Some actions require admin but not allowAdminChanges + $this->requireAdmin(false); + } else { + // All other actions require an admin & allowAdminChanges + $this->requireAdmin(); + } + + $this->readOnly = !Craft::$app->getConfig()->getGeneral()->allowAdminChanges; return true; } @@ -56,6 +67,7 @@ public function actionVolumeIndex(): Response { $variables = []; $variables['volumes'] = Craft::$app->getVolumes()->getAllVolumes(); + $variables['readOnly'] = $this->readOnly; return $this->renderTemplate('settings/assets/volumes/_index.twig', $variables); } @@ -71,7 +83,9 @@ public function actionVolumeIndex(): Response */ public function actionEditVolume(?int $volumeId = null, ?Volume $volume = null): Response { - $this->requireAdmin(); + if ($volumeId === null && $this->readOnly) { + throw new ForbiddenHttpException('Administrative changes are disallowed in this environment.'); + } $volumesServices = Craft::$app->getVolumes(); @@ -111,20 +125,11 @@ public function actionEditVolume(?int $volumeId = null, ?Volume $volume = null): ->all(); array_unshift($fsOptions, ['label' => Craft::t('app', 'Select a filesystem'), 'value' => '']); - return $this->asCpScreen() + $response = $this->asCpScreen() ->title($title) ->addCrumb(Craft::t('app', 'Settings'), 'settings') ->addCrumb(Craft::t('app', 'Assets'), 'settings/assets') ->addCrumb(Craft::t('app', 'Volumes'), 'settings/assets') - ->action('volumes/save-volume') - ->redirectUrl('settings/assets') - ->saveShortcutRedirectUrl('settings/assets/volumes/{id}') - ->addAltAction(Craft::t('app', 'Save and continue editing'), [ - 'redirect' => 'settings/assets/volumes/{id}', - 'shortcut' => true, - 'retainScroll' => true, - ]) - ->editUrl($volume->getCpEditUrl()) ->contentTemplate('settings/assets/volumes/_edit.twig', [ 'volumeId' => $volumeId, 'volume' => $volume, @@ -132,7 +137,25 @@ public function actionEditVolume(?int $volumeId = null, ?Volume $volume = null): 'typeName' => Asset::displayName(), 'lowerTypeName' => Asset::lowerDisplayName(), 'fsOptions' => $fsOptions, + 'readOnly' => $this->readOnly, ]); + + if (!$this->readOnly) { + $response + ->action('volumes/save-volume') + ->redirectUrl('settings/assets') + ->saveShortcutRedirectUrl('settings/assets/volumes/{id}') + ->addAltAction(Craft::t('app', 'Save and continue editing'), [ + 'redirect' => 'settings/assets/volumes/{id}', + 'shortcut' => true, + 'retainScroll' => true, + ]) + ->editUrl($volume->getCpEditUrl()); + } else { + $response->noticeHtml(Cp::allowAdminChangesReadOnlyNotice()); + } + + return $response; } /** diff --git a/src/fs/Local.php b/src/fs/Local.php index 646985fb313..92b20b837b6 100644 --- a/src/fs/Local.php +++ b/src/fs/Local.php @@ -143,6 +143,7 @@ public function getSettingsHtml(): ?string return Craft::$app->getView()->renderTemplate('_components/fs/Local/settings.twig', [ 'volume' => $this, + 'disabled' => !Craft::$app->getConfig()->getGeneral()->allowAdminChanges, ]); } diff --git a/src/templates/_components/fs/Local/settings.twig b/src/templates/_components/fs/Local/settings.twig index 5719cd02869..ef60f6341bf 100644 --- a/src/templates/_components/fs/Local/settings.twig +++ b/src/templates/_components/fs/Local/settings.twig @@ -13,4 +13,5 @@ placeholder: "/path/to/folder"|t('app'), errors: volume.getErrors('path'), data: {'error-key': 'path'}, + disabled: disabled, }) }} diff --git a/src/templates/settings/assets/transforms/_index.twig b/src/templates/settings/assets/transforms/_index.twig index 1e8f36674b5..8e71ba12cb4 100644 --- a/src/templates/settings/assets/transforms/_index.twig +++ b/src/templates/settings/assets/transforms/_index.twig @@ -1,5 +1,8 @@ {% extends "settings/assets/_layout" %} {% set selectedNavItem = 'transforms' %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} @@ -13,12 +16,18 @@ "No image transforms exist yet.", ]) %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} + {% block content %}
- + {% if not readOnly %} + + {% endif %} {% endblock %} {% set tableData = [] %} @@ -49,11 +58,16 @@ var columns = [ { name: 'format', title: Craft.t('app', 'Format'), } ]; -new Craft.VueAdminTable({ +let config = { columns: columns, container: '#transforms-vue-admin-table', - deleteAction: 'image-transforms/delete', emptyMessage: Craft.t('app', 'No image transforms exist yet.'), tableData: {{ tableData|json_encode|raw }}, - }); +}; + +if (!{{ readOnly }}) { + config['deleteAction'] = 'image-transforms/delete'; +} + +new Craft.VueAdminTable(config); {% endjs %} diff --git a/src/templates/settings/assets/transforms/_settings.twig b/src/templates/settings/assets/transforms/_settings.twig index e1b4b012b65..2f481138a0e 100644 --- a/src/templates/settings/assets/transforms/_settings.twig +++ b/src/templates/settings/assets/transforms/_settings.twig @@ -1,6 +1,9 @@ {% extends '_layouts/cp.twig' %} -{% set fullPageForm = true %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} +{% set fullPageForm = not readOnly %} {% set crumbs = [ { label: "Settings"|t('app'), url: url('settings') }, @@ -19,215 +22,231 @@ {% import '_includes/forms.twig' as forms %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} {% block content %} + {% if not readOnly %} {{ actionInput('image-transforms/save') }} {{ redirectInput('settings/assets/transforms') }} {% if transform.id %}{{ hiddenInput('transformId', transform.id) }}{% endif %} - - {{ forms.textField({ - first: true, - label: "Name"|t('app'), - id: 'name', - name: 'name', - value: transform.name, - errors: transform.getErrors('name'), - autofocus: true, - required: true, - }) }} - - {{ forms.textField({ - label: "Handle"|t('app'), - id: "handle", - name: "handle", - class: 'code', - autocorrect: false, - autocapitalize: false, - value: transform.handle, - errors: transform.getErrors('handle'), - required: true, - }) }} - - {% set modeInput %} -
- - - - - - - -
- {% endset %} - - {{ forms.field({ - label: "Mode"|t('app') - }, modeInput) }} - -
- {{ forms.colorField({ - label: 'Fill Color', - name: 'fill', - value: transform.mode == 'letterbox' and transform.fill != 'transparent' ? transform.fill, - errors: transform.getErrors('fill'), - }) }} + {% endif %} + + {{ forms.textField({ + first: true, + label: "Name"|t('app'), + id: 'name', + name: 'name', + value: transform.name, + errors: transform.getErrors('name'), + autofocus: true, + required: true, + disabled: readOnly, + }) }} + + {{ forms.textField({ + label: "Handle"|t('app'), + id: "handle", + name: "handle", + class: 'code', + autocorrect: false, + autocapitalize: false, + value: transform.handle, + errors: transform.getErrors('handle'), + required: true, + disabled: readOnly, + }) }} + + {% set modeInput %} +
+ + + + + + +
- - - - - - {{ forms.textField({ - label: "Width"|t('app'), - id: "width", - name: "width", - size: 5, - value: transform.width, - errors: transform.getErrors('width'), - }) }} - - {{ forms.textField({ - label: "Height"|t('app'), - id: "height", - name: "height", - size: 5, - value: transform.height, - errors: transform.getErrors('height'), - }) }} - - {{ forms.lightswitchField({ - label: 'Allow Upscaling'|t('app'), - id: 'upscale', - name: 'upscale', - on: transform.upscale ?? craft.app.config.general.upscaleImages, - errors: transform.getErrors('upscale'), + {% endset %} + + {{ forms.field({ + label: "Mode"|t('app') + }, modeInput) }} + +
+ {{ forms.colorField({ + label: 'Fill Color', + name: 'fill', + value: transform.mode == 'letterbox' and transform.fill != 'transparent' ? transform.fill, + errors: transform.getErrors('fill'), + disabled: readOnly, }) }} +
- {% embed '_includes/forms/field.twig' with { - label: 'Quality'|t('app'), - errors: transform.getErrors('quality'), - } %} - {% block input %} - {% import '_includes/forms.twig' as forms %} -
-
- {{ forms.select({ - id: 'quality-picker', - options: [ - {label: 'Auto'|t('app'), value: 0}, - ]|merge(qualityPickerOptions), - value: qualityPickerValue, - describedBy: describedBy, - }) }} -
-
- {{ forms.text({ - id: 'quality', - class: [ - 'ltr', - transform.quality == 0 ? 'hidden', - ]|filter, - name: 'quality', - value: transform.quality, - size: 5, - type: 'number', - min: 1, - max: 100, - describedBy: describedBy, - }) }} -
-
- {% endblock %} - {% endembed %} - + - {% set formatOptions = [ - {label: 'Auto', value: null}, - {label: 'jpg', value: 'jpg'}, - {label: 'png', value: 'png'}, - {label: 'gif', value: 'gif'}, - ] %} - - {% if transform.format == 'webp' or craft.app.images.supportsWebP %} - {% set formatOptions = formatOptions|merge([{label: 'webp', value: 'webp'}]) %} - {% endif %} - - {% if transform.format == 'avif' or craft.app.images.supportsAvif %} - {% set formatOptions = formatOptions|merge([{label: 'avif', value: 'avif'}]) %} - {% endif %} - + + + {{ forms.textField({ + label: "Width"|t('app'), + id: "width", + name: "width", + size: 5, + value: transform.width, + errors: transform.getErrors('width'), + disabled: readOnly, + }) }} + + {{ forms.textField({ + label: "Height"|t('app'), + id: "height", + name: "height", + size: 5, + value: transform.height, + errors: transform.getErrors('height'), + disabled: readOnly, + }) }} + + {{ forms.lightswitchField({ + label: 'Allow Upscaling'|t('app'), + id: 'upscale', + name: 'upscale', + on: transform.upscale ?? craft.app.config.general.upscaleImages, + errors: transform.getErrors('upscale'), + disabled: readOnly, + }) }} + + {% embed '_includes/forms/field.twig' with { + label: 'Quality'|t('app'), + errors: transform.getErrors('quality'), + } %} + {% block input %} + {% import '_includes/forms.twig' as forms %} +
+
+ {{ forms.select({ + id: 'quality-picker', + options: [ + {label: 'Auto'|t('app'), value: 0}, + ]|merge(qualityPickerOptions), + value: qualityPickerValue, + describedBy: describedBy, + disabled: readOnly, + }) }} +
+
+ {{ forms.text({ + id: 'quality', + class: [ + 'ltr', + transform.quality == 0 ? 'hidden', + ]|filter, + name: 'quality', + value: transform.quality, + size: 5, + type: 'number', + min: 1, + max: 100, + describedBy: describedBy, + disabled: readOnly, + }) }} +
+
+ {% endblock %} + {% endembed %} + + {{ forms.selectField({ + label: "Interlacing"|t('app'), + id: "interlace", + name: "interlace", + options: [ + {label: 'None'|t('app'), value: 'none'}, + {label: 'Line'|t('app'), value: 'line'}, + {label: 'Plane'|t('app'), value: 'plane'}, + {label: 'Partition'|t('app'), value: 'partition'}, + ], + value: transform.interlace ?? 'none', + errors: transform.getErrors('interlace'), + disabled: readOnly, + }) }} + + {% set formatOptions = [ + {label: 'Auto', value: null}, + {label: 'jpg', value: 'jpg'}, + {label: 'png', value: 'png'}, + {label: 'gif', value: 'gif'}, + ] %} + + {% if transform.format == 'webp' or craft.app.images.supportsWebP %} + {% set formatOptions = formatOptions|merge([{label: 'webp', value: 'webp'}]) %} + {% endif %} + + {% if transform.format == 'avif' or craft.app.images.supportsAvif %} + {% set formatOptions = formatOptions|merge([{label: 'avif', value: 'avif'}]) %} + {% endif %} + + {{ forms.selectField({ + label: "Image Format"|t('app'), + id: "format", + name: "format", + instructions: "The image format that transformed images should use."|t('app'), + value: transform.format, + errors: transform.getErrors('format'), + options: formatOptions, + disabled: readOnly, + }) }} {% endblock %} diff --git a/src/templates/settings/assets/volumes/_edit.twig b/src/templates/settings/assets/volumes/_edit.twig index 5c55c9d57cd..2bfd3c8e83f 100644 --- a/src/templates/settings/assets/volumes/_edit.twig +++ b/src/templates/settings/assets/volumes/_edit.twig @@ -1,5 +1,7 @@ {% import '_includes/forms.twig' as forms %} - +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} {% if not isNewVolume %}{{ hiddenInput('volumeId', volume.id) }}{% endif %} @@ -13,6 +15,7 @@ required: true, errors: volume.getErrors('name'), data: {'error-key': 'name'}, + disabled: readOnly, }) }} {{ forms.textField({ @@ -26,6 +29,7 @@ required: true, errors: volume.getErrors('handle'), data: {'error-key': 'handle'}, + disabled: readOnly, }) }}
@@ -40,6 +44,7 @@ options: fsOptions, errors: volume.getErrors('fsHandle'), data: {'error-key': 'fsHandle'}, + disabled: readOnly, }) }} {{ forms.autosuggestField({ @@ -51,6 +56,7 @@ value: volume.getSubpath(false, false), errors: volume.getErrors('subpath'), data: {'error-key': 'subpath'}, + disabled: readOnly, }) }} {{ forms.fsField({ @@ -63,6 +69,7 @@ options: [{label: 'Same as asset filesystem'|t('app'), value: null}]|merge(craft.cp.getFsOptions()), errors: volume.getErrors('transformFsHandle'), data: {'error-key': 'transformFsHandle'}, + disabled: readOnly, }) }} {{ forms.autosuggestField({ @@ -74,6 +81,7 @@ value: volume.getTransformSubpath(false, false), errors: volume.getErrors('transformSubpath'), data: {'error-key': 'transformSubpath'}, + disabled: readOnly, }) }} {% if craft.app.getIsMultiSite() %} @@ -97,7 +105,8 @@ ]|filter, value: volume.titleTranslationMethod, toggle: true, - targetPrefix: 'title-translation-method-' + targetPrefix: 'title-translation-method-', + disabled: readOnly, }) }}
@@ -113,6 +122,7 @@ value: volume.titleTranslationKeyFormat, errors: volume.getErrors('titleTranslationKeyFormat'), data: {'error-key': 'titleTranslationKeyFormat'}, + disabled: readOnly, }) }}
@@ -134,7 +144,8 @@ ]|filter, value: volume.altTranslationMethod, toggle: true, - targetPrefix: 'alt-translation-method-' + targetPrefix: 'alt-translation-method-', + disabled: readOnly, }) }}
@@ -150,6 +161,7 @@ value: volume.altTranslationKeyFormat, errors: volume.getErrors('altTranslationKeyFormat'), data: {'error-key': 'altTranslationKeyFormat'}, + disabled: readOnly, }) }}
{% endif %} @@ -159,6 +171,7 @@ {{ forms.fieldLayoutDesignerField({ fieldLayout: volume.getFieldLayout(), withCardViewDesigner: true, + disabled: readOnly, }) }} diff --git a/src/templates/settings/assets/volumes/_index.twig b/src/templates/settings/assets/volumes/_index.twig index 9bc0b293ad5..fa78f0ca5ed 100644 --- a/src/templates/settings/assets/volumes/_index.twig +++ b/src/templates/settings/assets/volumes/_index.twig @@ -1,5 +1,8 @@ {% extends "settings/assets/_layout" %} {% set selectedNavItem = 'volumes' %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} @@ -10,12 +13,18 @@ "No volumes exist yet." ]) %} +{% if readOnly %} + {% set contentNotice = craft.cp.allowAdminChangesReadOnlyNotice() %} +{% endif %} + {% block content %}
- + {% if not readOnly %} + + {% endif %} {% endblock %} {% set tableData = [] %} @@ -41,12 +50,17 @@ var columns = [ { name: '__slot:handle', title: Craft.t('app', 'Handle') }, ]; -new Craft.VueAdminTable({ +let config = { columns: columns, container: '#volumes-vue-admin-table', - deleteAction: 'volumes/delete-volume', emptyMessage: Craft.t('app', 'No volumes exist yet.'), - reorderAction: '{{ volumes|length > 1 ? 'volumes/reorder-volumes' : ''}}', tableData: {{ tableData|json_encode|raw }} -}); +} + +if (!{{ readOnly }}) { + config['deleteAction'] = 'volumes/delete-volume'; + config['reorderAction'] = '{{ volumes|length > 1 ? 'volumes/reorder-volumes' : ''}}'; +} + +new Craft.VueAdminTable(config); {% endjs %} diff --git a/src/templates/settings/filesystems/_edit.twig b/src/templates/settings/filesystems/_edit.twig index 3250dd2cf70..5c3873d9607 100644 --- a/src/templates/settings/filesystems/_edit.twig +++ b/src/templates/settings/filesystems/_edit.twig @@ -1,4 +1,7 @@ {% import '_includes/forms.twig' as forms %} +{% if readOnly is not defined %} + {% set readOnly = not craft.app.config.general.allowAdminChanges %} +{% endif %} {% if oldHandle %}{{ hiddenInput('oldHandle', oldHandle) }}{% endif %} @@ -12,6 +15,7 @@ required: true, errors: (filesystem is defined ? filesystem.getErrors('name') : null), data: {'error-key': 'name'}, + disabled: readOnly, }) }} {{ forms.textField({ @@ -26,6 +30,7 @@ required: true, errors: (filesystem is defined ? filesystem.getErrors('handle') : null), data: {'error-key': 'handle'}, + disabled: readOnly, }) }}
@@ -38,20 +43,29 @@ name: 'type', options: fsOptions, value: className(filesystem), - toggle: true + toggle: true, + disabled: readOnly, }) }} {% endif %} {% for fsType in fsTypes %} {% set isCurrent = (fsType == className(filesystem)) %} + {% set classes = [] %} + {% if not isCurrent %} + {% set classes = classes|merge(['hidden']) %} + {% endif %} + {% if readOnly %} + {% set classes = classes|merge(['disabled']) %} + {% endif %} -