From a2ef8806c58656bac4130fb6d9751d53bcd1c11a Mon Sep 17 00:00:00 2001 From: benjamin Date: Wed, 12 Mar 2025 08:24:28 +0000 Subject: [PATCH 1/8] riverlea - add Stream entity --- .../CRM/riverlea/BAO/RiverleaStream.php | 7 + .../CRM/riverlea/DAO/RiverleaStream.php | 20 +++ ext/riverlea/CRM/riverlea/Upgrader.php | 137 +++++++++++++++ .../RiverleaStream/GetWithFileContent.php | 71 ++++++++ .../Api4/Action/RiverleaStream/Render.php | 162 ++++++++++++++++++ ext/riverlea/Civi/Api4/RiverleaStream.php | 15 ++ ext/riverlea/Civi/Riverlea/DynamicCss.php | 108 ++++-------- ext/riverlea/info.xml | 3 + ext/riverlea/riverlea.civix.php | 35 ++++ ext/riverlea/riverlea.php | 74 ++++---- .../schema/RiverleaStream.entityType.php | 137 +++++++++++++++ 11 files changed, 670 insertions(+), 99 deletions(-) create mode 100644 ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php create mode 100644 ext/riverlea/CRM/riverlea/DAO/RiverleaStream.php create mode 100644 ext/riverlea/CRM/riverlea/Upgrader.php create mode 100644 ext/riverlea/Civi/Api4/Action/RiverleaStream/GetWithFileContent.php create mode 100644 ext/riverlea/Civi/Api4/Action/RiverleaStream/Render.php create mode 100644 ext/riverlea/Civi/Api4/RiverleaStream.php create mode 100644 ext/riverlea/schema/RiverleaStream.entityType.php diff --git a/ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php b/ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php new file mode 100644 index 000000000000..a4dfbf796429 --- /dev/null +++ b/ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php @@ -0,0 +1,7 @@ +executeSqlFile('sql/my_install.sql'); + // } + + /** + * Example: Work with entities usually not available during the install step. + * + * This method can be used for any post-install tasks. For example, if a step + * of your installation depends on accessing an entity that is itself + * created during the installation (e.g., a setting or a managed entity), do + * so here to avoid order of operation problems. + */ + // public function postInstall(): void { + // $customFieldId = civicrm_api3('CustomField', 'getvalue', array( + // 'return' => array("id"), + // 'name' => "customFieldCreatedViaManagedHook", + // )); + // civicrm_api3('Setting', 'create', array( + // 'myWeirdFieldSetting' => array('id' => $customFieldId, 'weirdness' => 1), + // )); + // } + + /** + * Example: Run an external SQL script when the module is uninstalled. + * + * Note that if a file is present sql\auto_uninstall that will run regardless of this hook. + */ + // public function uninstall(): void { + // $this->executeSqlFile('sql/my_uninstall.sql'); + // } + + /** + * Example: Run a simple query when a module is enabled. + */ + // public function enable(): void { + // CRM_Core_DAO::executeQuery('UPDATE foo SET is_active = 1 WHERE bar = "whiz"'); + // } + + /** + * Example: Run a simple query when a module is disabled. + */ + // public function disable(): void { + // CRM_Core_DAO::executeQuery('UPDATE foo SET is_active = 0 WHERE bar = "whiz"'); + // } + + /** + * Example: Run a couple simple queries. + * + * @return TRUE on success + * @throws CRM_Core_Exception + */ + // public function upgrade_4200(): bool { + // $this->ctx->log->info('Applying update 4200'); + // CRM_Core_DAO::executeQuery('UPDATE foo SET bar = "whiz"'); + // CRM_Core_DAO::executeQuery('DELETE FROM bang WHERE willy = wonka(2)'); + // return TRUE; + // } + + /** + * Example: Run an external SQL script. + * + * @return TRUE on success + * @throws CRM_Core_Exception + */ + // public function upgrade_4201(): bool { + // $this->ctx->log->info('Applying update 4201'); + // // this path is relative to the extension base dir + // $this->executeSqlFile('sql/upgrade_4201.sql'); + // return TRUE; + // } + + /** + * Example: Run a slow upgrade process by breaking it up into smaller chunk. + * + * @return TRUE on success + * @throws CRM_Core_Exception + */ + // public function upgrade_4202(): bool { + // $this->ctx->log->info('Planning update 4202'); // PEAR Log interface + + // $this->addTask(E::ts('Process first step'), 'processPart1', $arg1, $arg2); + // $this->addTask(E::ts('Process second step'), 'processPart2', $arg3, $arg4); + // $this->addTask(E::ts('Process second step'), 'processPart3', $arg5); + // return TRUE; + // } + // public function processPart1($arg1, $arg2) { sleep(10); return TRUE; } + // public function processPart2($arg3, $arg4) { sleep(10); return TRUE; } + // public function processPart3($arg5) { sleep(10); return TRUE; } + + /** + * Example: Run an upgrade with a query that touches many (potentially + * millions) of records by breaking it up into smaller chunks. + * + * @return TRUE on success + * @throws CRM_Core_Exception + */ + // public function upgrade_4203(): bool { + // $this->ctx->log->info('Planning update 4203'); // PEAR Log interface + + // $minId = CRM_Core_DAO::singleValueQuery('SELECT coalesce(min(id),0) FROM civicrm_contribution'); + // $maxId = CRM_Core_DAO::singleValueQuery('SELECT coalesce(max(id),0) FROM civicrm_contribution'); + // for ($startId = $minId; $startId <= $maxId; $startId += self::BATCH_SIZE) { + // $endId = $startId + self::BATCH_SIZE - 1; + // $title = E::ts('Upgrade Batch (%1 => %2)', array( + // 1 => $startId, + // 2 => $endId, + // )); + // $sql = ' + // UPDATE civicrm_contribution SET foobar = apple(banana()+durian) + // WHERE id BETWEEN %1 and %2 + // '; + // $params = array( + // 1 => array($startId, 'Integer'), + // 2 => array($endId, 'Integer'), + // ); + // $this->addTask($title, 'executeSql', $sql, $params); + // } + // return TRUE; + // } + +} diff --git a/ext/riverlea/Civi/Api4/Action/RiverleaStream/GetWithFileContent.php b/ext/riverlea/Civi/Api4/Action/RiverleaStream/GetWithFileContent.php new file mode 100644 index 000000000000..bebb08b9b67d --- /dev/null +++ b/ext/riverlea/Civi/Api4/Action/RiverleaStream/GetWithFileContent.php @@ -0,0 +1,71 @@ +getPath($extension, $path); + + if (is_file($finalPath)) { + // File exists and is a file? Return it! + return file_get_contents($finalPath) ?? ''; + } + return ''; + } + +} diff --git a/ext/riverlea/Civi/Api4/Action/RiverleaStream/Render.php b/ext/riverlea/Civi/Api4/Action/RiverleaStream/Render.php new file mode 100644 index 000000000000..cbbdd3eed518 --- /dev/null +++ b/ext/riverlea/Civi/Api4/Action/RiverleaStream/Render.php @@ -0,0 +1,162 @@ +darkMode) { + $this->darkMode = $this->getDarkModeDefault($stream); + } + + $content[] = self::concatStreamCss($stream); + + switch ($this->darkMode ?? NULL) { + case 'light': + // tell OS we want light for system elements + $content[] = ":root { color-scheme: light; }"; + break; + + case 'dark': + // tell OS we want dark for system elements + $content[] = ":root { color-scheme: dark; }"; + // add stream dark unconditionally + $content[] = self::concatStreamCss($stream, TRUE); + break; + + case 'inherit': + default: + // tell OS we are happy with light or dark for system elements + $content[] = ":root { color-scheme: light dark; }"; + // add stream dark vars wrapped inside a media query + $content[] = '@media (prefers-color-scheme: dark) {'; + $content[] = self::concatStreamCss($stream, TRUE); + $content[] = '}'; + break; + } + + return [ + 'content' => implode("\n", $content), + ]; + } + + private function getDarkModeDefault(array $stream) { + if ($this->isFrontend) { + return $stream['dark_frontend'] ?? \Civi::settings()->get('riverlea_dark_mode_frontend'); + } + else { + return $stream['dark_backend'] ?? \Civi::settings()->get('riverlea_dark_mode_backend'); + } + } + + /** + * This replaces stream meta with its dark version, ready to pass + * to concatStreamCss / getStreamCssFromFile + */ + private static function getDarkStream(array $stream): array { + $stream['label'] = $stream['label'] . " Dark Styles"; + $stream['vars'] = $stream['vars_dark']; + $stream['css_file'] = $stream['css_file_dark']; + $stream['custom_css'] = $stream['custom_css_dark']; + + return $stream; + } + + /** + * @param array $stream the stream meta + * @param bool $darkMode whether to use the regular stream meta, or dark version + * + * @return string + */ + private static function concatStreamCss(array $stream, bool $darkMode = FALSE): string { + if ($darkMode) { + $stream = self::getDarkStream($stream); + } + + $content = []; + + if ($stream['css_file']) { + $content[] = "/* {$stream['label']} file: {$stream['css_file']} */"; + $content[] = self::getStreamCssFromFile($stream); + } + + if ($stream['vars']) { + $content[] = "/* {$stream['label']} vars */"; + + $content[] = ":root {"; + foreach ($stream['vars'] as $var => $value) { + $content[] = "{$var}: {$value};"; + } + $content[] = "}"; + } + + if ($stream['custom_css']) { + $content[] = "/* {$stream['label']} custom css */"; + $content[] = $stream['custom_css']; + } + + return implode("\n", $content); + } + + /** + * @return string + */ + private static function getStreamCssFromFile(array $stream): string { + return GetWithFileContent::getFileContent($stream['css_file'], $stream['extension'], $stream['file_prefix']); + } + +} diff --git a/ext/riverlea/Civi/Api4/RiverleaStream.php b/ext/riverlea/Civi/Api4/RiverleaStream.php new file mode 100644 index 000000000000..9cf285612fe9 --- /dev/null +++ b/ext/riverlea/Civi/Api4/RiverleaStream.php @@ -0,0 +1,15 @@ +get('riverlea_dark_mode_frontend') : - \Civi::settings()->get('riverlea_dark_mode_backend'); + public function getAvailableStreamMeta(): array { + $streams = \Civi::$statics['riverlea_streams'] ?? NULL; + + if (is_null($streams)) { + try { + $streams = (array) \Civi\Api4\RiverleaStream::get(FALSE) + ->addSelect('name', 'label', 'extension', 'file_prefix', 'parent_id', 'id', 'modified_date') + ->execute() + ->indexBy('name'); + + \Civi::$statics['riverlea_streams'] = $streams; + } + catch (\CRM_Core_Exception $e) { + \Civi::log()->warning('Error loading Riverlea stream meta'); + return []; + } + } + + return $streams; + } + + public function getCssParams(): array { + $stream = \Civi::service('themes')->getActiveThemeKey(); + + // we add the stream modified date to asset params as a cache buster + $streamMeta = self::getAvailableStreamMeta()[$stream] ?? []; + $streamModified = $streamMeta['modified_date'] ?? NULL; return [ - 'stream' => \Civi::service('themes')->getActiveThemeKey(), - 'dark' => $darkModeSetting, + 'stream' => $stream, + 'modified' => $streamModified, + 'is_frontend' => \CRM_Utils_System::isFrontendPage(), ]; } @@ -45,75 +69,19 @@ public static function getCssParams(): array { * @see CRM_Utils_hook::buildAsset() * @see \Civi\Core\AssetBuilder */ - public static function buildAssetCss($e) { + public function buildAssetCss($e) { if ($e->asset !== static::CSS_FILE) { return; } $e->mimeType = 'text/css'; - $params = $e->params; - - $stream = $params['stream'] ?? 'empty'; - - $content = []; - - // add base vars for the stream - $content[] = self::getCSSFromFile('_variables.css', $stream); - - switch ($params['dark'] ?? NULL) { - case 'light': - // tell OS we want light for system elements - $content[] = ":root { color-scheme: light; }"; - break; - - case 'dark': - // tell OS we want dark for system elements - $content[] = ":root { color-scheme: dark; }"; - // add stream dark vars unconditionally - $content[] = self::getCSSFromFile('_dark.css', $stream); - break; - - case 'inherit': - default: - // tell OS we are happy with light or dark for system elements - $content[] = ":root { color-scheme: light dark; }"; - // add stream dark vars wrapped inside a media query - $content[] = '@media (prefers-color-scheme: dark) {'; - $content[] = self::getCSSFromFile('_dark.css', $stream); - $content[] = '}'; - break; - } - - $e->content = implode("\n", $content); - } + $render = \Civi\Api4\RiverleaStream::render(FALSE) + ->addWhere('name', '=', $e->params['stream']) + ->setIsFrontend($e->params['is_frontend']) + ->execute() + ->first(); - /** - * Check file exists and return contents or empty string - * - * @param string $cssFileName The name of the css file (eg. _variables.css) - * @param string $stream The name of the riverlea stream (eg. walbrook) - * - * @return string - */ - private static function getCSSFromFile(string $cssFileName, string $stream): string { - $res = \Civi::resources(); - $theme = \Civi::service('themes')->get($stream); - $file = ''; - // For riverlea themes CSS should be located in stream/streamname/css - prefix="stream/streamname/" - if (isset($theme['prefix'])) { - $file .= $theme['prefix']; - } - // Append css dir and filename so we end up with stream/streamname/css/filename.css - $file .= 'css/' . $cssFileName; - $file = $res->filterMinify($theme['ext'], $file); - - // Now get the full path for the css file - $filePath = $res->getPath($theme['ext'], $file); - if (is_file($filePath)) { - // File exists and is a file? Return it! - return file_get_contents($filePath) ?? ''; - } - return ''; + $e->content = $render['content'] ?? ''; } } diff --git a/ext/riverlea/info.xml b/ext/riverlea/info.xml index f05fb3112769..8ad7ab5dd3d6 100755 --- a/ext/riverlea/info.xml +++ b/ext/riverlea/info.xml @@ -20,6 +20,7 @@ A cross-CMS CiviCRM theme framework. + CRM/Riverlea @@ -31,5 +32,7 @@ setting-admin@1.0.1 mgd-php@1.0.0 scan-classes@1.0.0 + entity-types-php@2.0.0 + CiviMix\Schema\Riverlea\AutomaticUpgrader diff --git a/ext/riverlea/riverlea.civix.php b/ext/riverlea/riverlea.civix.php index 01dd89c91247..8fe8da11c003 100755 --- a/ext/riverlea/riverlea.civix.php +++ b/ext/riverlea/riverlea.civix.php @@ -75,10 +75,45 @@ public static function findClass($suffix) { return self::CLASS_PREFIX . '_' . str_replace('\\', '_', $suffix); } + /** + * @return \CiviMix\Schema\SchemaHelperInterface + */ + public static function schema() { + if (!isset($GLOBALS['CiviMixSchema'])) { + pathload()->loadPackage('civimix-schema@5', TRUE); + } + return $GLOBALS['CiviMixSchema']->getHelper(static::LONG_NAME); + } + } use CRM_riverlea_ExtensionUtil as E; +spl_autoload_register('_riverlea_civix_class_loader', TRUE, TRUE); + +function _riverlea_civix_class_loader($class) { + if ($class === 'CRM_riverlea_DAO_Base') { + if (version_compare(CRM_Utils_System::version(), '5.74.beta', '>=')) { + class_alias('CRM_Core_DAO_Base', 'CRM_riverlea_DAO_Base'); + // ^^ Materialize concrete names -- encourage IDE's to pick up on this association. + } + else { + $realClass = 'CiviMix\\Schema\\Riverlea\\DAO'; + class_alias($realClass, $class); + // ^^ Abstract names -- discourage IDE's from picking up on this association. + } + return; + } + + // This allows us to tap-in to the installation process (without incurring real file-reads on typical requests). + if (strpos($class, 'CiviMix\\Schema\\Riverlea\\') === 0) { + // civimix-schema@5 is designed for backported use in download/activation workflows, + // where new revisions may become dynamically available. + pathload()->loadPackage('civimix-schema@5', TRUE); + CiviMix\Schema\loadClass($class); + } +} + /** * (Delegated) Implements hook_civicrm_config(). * diff --git a/ext/riverlea/riverlea.php b/ext/riverlea/riverlea.php index 555aa4a7a9df..2ce580b33993 100755 --- a/ext/riverlea/riverlea.php +++ b/ext/riverlea/riverlea.php @@ -4,39 +4,55 @@ use CRM_riverlea_ExtensionUtil as E; /** - * Supports multiple theme variations/streams. + * Supply available streams to the theme hook + * + * Note: if this looks labour intensive, don't worry - the output + * is cached in \Civi\Core\Themes */ function riverlea_civicrm_themes(&$themes) { - $themes['minetta'] = array( - 'ext' => 'riverlea', - 'title' => 'Minetta (RiverLea ~Greenwich)', - 'prefix' => 'streams/minetta/', - 'search_order' => array('minetta', '_riverlea_core_', '_fallback_'), - ); - $themes['walbrook'] = array( - 'ext' => 'riverlea', - 'title' => 'Walbrook (RiverLea ~Shoreditch/Island)', - 'prefix' => 'streams/walbrook/', - 'search_order' => array('walbrook', '_riverlea_core_', '_fallback_'), - ); - $themes['hackneybrook'] = array( - 'ext' => 'riverlea', - 'title' => 'Hackney Brook (RiverLea ~Finsbury Park)', - 'prefix' => 'streams/hackneybrook/', - 'search_order' => array('hackneybrook', '_riverlea_core_', '_fallback_'), - ); - $themes['thames'] = array( - 'ext' => 'riverlea', - 'title' => 'Thames (RiverLea ~Aah)', - 'prefix' => 'streams/thames/', - 'search_order' => array('thames', '_riverlea_core_', '_fallback_'), - ); - $themes['_riverlea_core_'] = array( + // always add (hidden) Riverlea base theme + $themes['_riverlea_core_'] = [ 'ext' => 'riverlea', 'title' => 'Riverlea: base theme', 'prefix' => 'core/', - 'search_order' => array('_riverlea_core_', '_fallback_'), - ); + 'search_order' => ['_riverlea_core_', '_fallback_'], + ]; + + try { + $streams = \Civi::service('riverlea.dynamic_css')->getAvailableStreamMeta(); + } + catch (\CRM_Core_Exception $e) { + // dont crash the whole hook if Riverlea is broken + \CRM_Core_Session::setStatus('Error occured making Riverlea streams available to the theme engine: ' . $e->getMessage()); + return; + } + + $streamsById = array_column($streams, NULL, 'id'); + + foreach ($streams as $name => $stream) { + $themeMeta = [ + 'title' => $stream['label'], + 'search_order' => [], + ]; + + $extension = $stream['extension']; + + // we only add the stream itself to the search order if + // it has an extension (which indicates it may have its own + // file overrides) + if ($extension) { + $themeMeta['search_order'][] = $name; + + // used to resolve files from this stream + $themeMeta['ext'] = $extension; + $themeMeta['prefix'] = $stream['file_prefix'] ?? ''; + } + + $themeMeta['search_order'][] = '_riverlea_core_'; + $themeMeta['search_order'][] = '_fallback_'; + + $themes[$name] = $themeMeta; + } } /** @@ -77,7 +93,7 @@ function riverlea_civicrm_alterBundle(CRM_Core_Resources_Bundle $bundle) { // get DynamicCss asset $bundle->addStyleUrl(\Civi::service('asset_builder')->getUrl( \Civi\Riverlea\DynamicCss::CSS_FILE, - \Civi\Riverlea\DynamicCss::getCssParams() + \Civi::service('riverlea.dynamic_css')->getCssParams() )); } } diff --git a/ext/riverlea/schema/RiverleaStream.entityType.php b/ext/riverlea/schema/RiverleaStream.entityType.php new file mode 100644 index 000000000000..12b5c2fe2489 --- /dev/null +++ b/ext/riverlea/schema/RiverleaStream.entityType.php @@ -0,0 +1,137 @@ + 'RiverleaStream', + 'table' => 'civicrm_riverlea_stream', + 'class' => 'CRM_riverlea_DAO_RiverleaStream', + 'getInfo' => fn() => [ + 'title' => E::ts('RiverleaStream'), + 'title_plural' => E::ts('RiverleaStreams'), + 'description' => E::ts('Streams are configurable themes in the Riverlea Theme Framework'), + 'log' => TRUE, + ], + 'getIndices' => fn() => [ + 'index_stream_name' => [ + 'fields' => [ + 'name' => TRUE, + ], + 'unique' => TRUE, + 'add' => '6.1', + ], + ], + 'getPaths' => fn() => [ + // 'add' => 'civicrm/admin/riverlea/stream/create', + // 'update' => 'civicrm/admin/riverlea/stream/update#?RiverleaStream=[id]', + // 'delete' => 'civicrm/contact/view/delete?reset=1&delete=1&cid=[id]', + ], + 'getFields' => fn() => [ + 'id' => [ + 'title' => E::ts('ID'), + 'sql_type' => 'int unsigned', + 'input_type' => 'Number', + 'required' => TRUE, + 'description' => E::ts('Unique RiverleaStream ID'), + 'primary_key' => TRUE, + 'auto_increment' => TRUE, + ], + 'name' => [ + 'title' => ts('Machine-Name'), + 'sql_type' => 'varchar(255)', + 'input_type' => 'Text', + 'description' => ts('Machine-name for this stream.'), + ], + 'label' => [ + 'title' => ts('Label'), + 'sql_type' => 'varchar(512)', + 'input_type' => 'Text', + 'required' => TRUE, + 'description' => ts('User-facing name for this stream'), + ], + 'description' => [ + 'title' => E::ts('Description'), + 'sql_type' => 'text', + 'input_type' => 'TextArea', + 'description' => E::ts('Description of this stream'), + 'default' => NULL, + ], + 'is_reserved' => [ + 'title' => E::ts('Is Reserved?'), + 'description' => E::ts('Reserved streams are not editable through the UI'), + 'sql_type' => 'boolean', + 'input_type' => 'CheckBox', + 'required' => TRUE, + 'default' => FALSE, + ], + 'extension' => [ + 'title' => ts('Extension'), + 'sql_type' => 'varchar(255)', + 'input_type' => 'Select', + 'description' => ts('Extension that provides this stream.'), + 'default' => NULL, + // @todo why not work? + // 'entity_reference' => [ + // 'entity' => 'Extension', + // 'key' => 'file', + // 'on_delete' => 'CASCADE', + // ], + ], + 'file_prefix' => [ + 'title' => ts('Extension File Prefix'), + 'sql_type' => 'varchar(255)', + 'input_type' => 'Text', + 'description' => ts('File prefix to stream files within extension'), + 'default' => NULL, + ], + 'css_file' => [ + 'title' => ts('CSS File'), + 'description' => ts('A file containing stream css - path should be relative to the extension and file_prefix'), + 'sql_type' => 'varchar(512)', + 'input_type' => 'Text', + 'default' => NULL, + ], + 'css_file_dark' => [ + 'title' => ts('Dark-mode CSS File'), + 'description' => ts('A file containing stream css for darkmode - path should be relative to the extension and file_prefix'), + 'sql_type' => 'varchar(512)', + 'input_type' => 'Text', + 'default' => NULL, + ], + 'vars' => [ + 'title' => E::ts('Variable Settings'), + 'sql_type' => 'text', + 'description' => E::ts('Variable declarations for this stream'), + 'default' => NULL, + 'serialize' => CRM_Core_DAO::SERIALIZE_JSON, + ], + 'vars_dark' => [ + 'title' => E::ts('Dark-mode Variable Settings'), + 'sql_type' => 'text', + 'description' => E::ts('Variable declaration overrides for the dark mode of this stream'), + 'default' => NULL, + 'serialize' => CRM_Core_DAO::SERIALIZE_JSON, + ], + 'custom_css' => [ + 'title' => E::ts('Custom CSS'), + 'sql_type' => 'text', + 'input_type' => 'TextArea', + 'description' => E::ts('Custom css for this stream'), + 'default' => NULL, + ], + 'custom_css_dark' => [ + 'title' => E::ts('Dark-mode Custom CSS'), + 'sql_type' => 'text', + 'input_type' => 'TextArea', + 'description' => E::ts('Custom css for the darkmode of this stream'), + 'default' => NULL, + ], + 'modified_date' => [ + 'title' => ts('Modified Date'), + 'sql_type' => 'timestamp', + 'input_type' => 'Select Date', + 'readonly' => TRUE, + 'description' => ts('When the stream was last modified - helps with cache busting.'), + 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', + ], + ], +]; From 507504bf6e6856f33bec2f00fbe758e1fe5241fc Mon Sep 17 00:00:00 2001 From: benjamin Date: Wed, 12 Mar 2025 08:26:53 +0000 Subject: [PATCH 2/8] riverlea - generate stream name from label --- ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php b/ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php index a4dfbf796429..e7effa7785b7 100644 --- a/ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php +++ b/ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php @@ -1,7 +1,15 @@ action === 'create' && !$event->params['name']) { + $event->params['name'] = \CRM_Utils_String::munge($event->params['label']); + } + } } From 6f81a52f5128240f70376046d73c2159a8557d4a Mon Sep 17 00:00:00 2001 From: benjamin Date: Wed, 12 Mar 2025 08:27:56 +0000 Subject: [PATCH 3/8] riverlea - clear theme cache after editing streams --- Civi/Core/Themes.php | 7 +++++++ ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/Civi/Core/Themes.php b/Civi/Core/Themes.php index 05469ba93710..340740ca27e8 100644 --- a/Civi/Core/Themes.php +++ b/Civi/Core/Themes.php @@ -265,4 +265,11 @@ public function cssId($cssExt, $cssFile) { return ($cssExt === 'civicrm') ? $cssFile : "$cssExt-$cssFile"; } + /** + * Clear the cache + */ + public function clearCache() { + $this->cache->clear(); + } + } diff --git a/ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php b/ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php index e7effa7785b7..5d06d0bdccc7 100644 --- a/ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php +++ b/ext/riverlea/CRM/riverlea/BAO/RiverleaStream.php @@ -3,6 +3,7 @@ use CRM_riverlea_ExtensionUtil as E; use Civi\Core\HookInterface; use Civi\Core\Event\PreEvent; +use Civi\Core\Event\PostEvent; class CRM_riverlea_BAO_RiverleaStream extends CRM_riverlea_DAO_RiverleaStream implements HookInterface { @@ -12,4 +13,9 @@ public static function self_hook_civicrm_pre(PreEvent $event): void { $event->params['name'] = \CRM_Utils_String::munge($event->params['label']); } } + + public static function self_hook_civicrm_postCommit(PostEvent $event): void { + \Civi::service('themes')->clear(); + } + } From 1fe6393e9713f1e7586d7140936895b5074c72bf Mon Sep 17 00:00:00 2001 From: benjamin Date: Wed, 12 Mar 2025 08:29:39 +0000 Subject: [PATCH 4/8] riverlea - add managed records for base streams --- .../RiverleaStream_HackneyBrook.mgd.php | 28 ++++++++++++++++++ .../managed/RiverleaStream_Minetta.mgd.php | 29 +++++++++++++++++++ .../managed/RiverleaStream_Thames.mgd.php | 27 +++++++++++++++++ .../managed/RiverleaStream_Walbrook.mgd.php | 27 +++++++++++++++++ .../streams/minetta/css/_variables.css | 3 -- 5 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 ext/riverlea/managed/RiverleaStream_HackneyBrook.mgd.php create mode 100644 ext/riverlea/managed/RiverleaStream_Minetta.mgd.php create mode 100644 ext/riverlea/managed/RiverleaStream_Thames.mgd.php create mode 100644 ext/riverlea/managed/RiverleaStream_Walbrook.mgd.php diff --git a/ext/riverlea/managed/RiverleaStream_HackneyBrook.mgd.php b/ext/riverlea/managed/RiverleaStream_HackneyBrook.mgd.php new file mode 100644 index 000000000000..b492b4c08cdf --- /dev/null +++ b/ext/riverlea/managed/RiverleaStream_HackneyBrook.mgd.php @@ -0,0 +1,28 @@ + 'RiverleaStream_HackneyBrook', + 'entity' => 'RiverleaStream', + 'update' => 'always', + 'cleanup' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'hackneybrook', + 'label' => E::ts('Hackney Brook'), + 'description' => 'named after the Hackney Brook, a tributary of the River Lea that ran through Finsbury Park', + 'is_reserved' => TRUE, + 'extension' => 'riverlea', + 'file_prefix' => 'streams/hackneybrook/', + 'css_file' => 'css/_variables.css', + 'css_file_dark' => 'css/_dark.css', + 'vars' => [], + 'vars_dark' => [], + ], + 'match' => ['name'], + ], + ], +]; diff --git a/ext/riverlea/managed/RiverleaStream_Minetta.mgd.php b/ext/riverlea/managed/RiverleaStream_Minetta.mgd.php new file mode 100644 index 000000000000..b2bd3675ce8f --- /dev/null +++ b/ext/riverlea/managed/RiverleaStream_Minetta.mgd.php @@ -0,0 +1,29 @@ + 'RiverleaStream_Minetta', + 'entity' => 'RiverleaStream', + 'update' => 'always', + 'cleanup' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'minetta', + 'description' => 'Generic CiviCRM UI, somewhat familiar to users of CiviCRM since 2014. Named after Minetta Creek, which runs under Greenwich, New York', + 'label' => E::ts('Minetta'), + 'is_reserved' => TRUE, + 'extension' => 'riverlea', + 'file_prefix' => 'streams/minetta/', + 'css_file' => 'css/_variables.css', + 'css_file_dark' => 'css/_dark.css', + 'vars' => [ + '--crm-version' => "'Minetta, v' var(--crm-release)", + ], + ], + 'match' => ['name'], + ], + ], +]; diff --git a/ext/riverlea/managed/RiverleaStream_Thames.mgd.php b/ext/riverlea/managed/RiverleaStream_Thames.mgd.php new file mode 100644 index 000000000000..d6fe5f452abc --- /dev/null +++ b/ext/riverlea/managed/RiverleaStream_Thames.mgd.php @@ -0,0 +1,27 @@ + 'RiverleaStream_Thames', + 'entity' => 'RiverleaStream', + 'update' => 'always', + 'cleanup' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'thames', + 'description' => 'Aaaaah', + 'label' => E::ts('Thames'), + 'is_reserved' => TRUE, + 'extension' => 'riverlea', + 'file_prefix' => 'streams/thames/', + 'css_file' => 'css/_variables.css', + 'css_file_dark' => 'css/_dark.css', + 'vars' => [], + ], + 'match' => ['name'], + ], + ], +]; diff --git a/ext/riverlea/managed/RiverleaStream_Walbrook.mgd.php b/ext/riverlea/managed/RiverleaStream_Walbrook.mgd.php new file mode 100644 index 000000000000..4924a3c495bb --- /dev/null +++ b/ext/riverlea/managed/RiverleaStream_Walbrook.mgd.php @@ -0,0 +1,27 @@ + 'RiverleaStream_Walbrook', + 'entity' => 'RiverleaStream', + 'update' => 'always', + 'cleanup' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'walbrook', + 'description' => 'Based on Shoreditch theme. Named after after River Walbrook, which runs under Shoreditch, London', + 'label' => E::ts('Walbrook'), + 'is_reserved' => TRUE, + 'extension' => 'riverlea', + 'file_prefix' => 'streams/walbrook/', + 'css_file' => 'css/_variables.css', + 'css_file_dark' => 'css/_dark.css', + 'vars' => [], + ], + 'match' => ['name'], + ], + ], +]; diff --git a/ext/riverlea/streams/minetta/css/_variables.css b/ext/riverlea/streams/minetta/css/_variables.css index 64f8073ce95c..bceec0776c32 100644 --- a/ext/riverlea/streams/minetta/css/_variables.css +++ b/ext/riverlea/streams/minetta/css/_variables.css @@ -4,9 +4,6 @@ Riverlea version: 1.3.8; */ -:root { - --crm-version: 'Minetta, v' var(--crm-release); -} .contactCardRight:has(#crm-contact-thumbnail) .float-left { width: calc(100% - var(--crm-flex-gap) - var(--crm-dash-image-size)); } From d4899449869a861677355328f2a8192d2afeaaaf Mon Sep 17 00:00:00 2001 From: benjamin Date: Tue, 20 May 2025 21:11:23 +0100 Subject: [PATCH 5/8] riverlea - add stream entity upgrader --- ext/riverlea/CRM/riverlea/Upgrader.php | 131 +---------------- .../1000-RiverleaStream.entityType.php | 137 ++++++++++++++++++ 2 files changed, 141 insertions(+), 127 deletions(-) create mode 100644 ext/riverlea/schema/upgrader/1000-RiverleaStream.entityType.php diff --git a/ext/riverlea/CRM/riverlea/Upgrader.php b/ext/riverlea/CRM/riverlea/Upgrader.php index 8116729c803e..283ce9c869b5 100644 --- a/ext/riverlea/CRM/riverlea/Upgrader.php +++ b/ext/riverlea/CRM/riverlea/Upgrader.php @@ -6,132 +6,9 @@ */ class CRM_riverlea_Upgrader extends \CRM_Extension_Upgrader_Base { - // By convention, functions that look like "function upgrade_NNNN()" are - // upgrade tasks. They are executed in order (like Drupal's hook_update_N). - - /** - * Example: Run an external SQL script when the module is installed. - * - * Note that if a file is present sql\auto_install that will run regardless of this hook. - */ - // public function install(): void { - // $this->executeSqlFile('sql/my_install.sql'); - // } - - /** - * Example: Work with entities usually not available during the install step. - * - * This method can be used for any post-install tasks. For example, if a step - * of your installation depends on accessing an entity that is itself - * created during the installation (e.g., a setting or a managed entity), do - * so here to avoid order of operation problems. - */ - // public function postInstall(): void { - // $customFieldId = civicrm_api3('CustomField', 'getvalue', array( - // 'return' => array("id"), - // 'name' => "customFieldCreatedViaManagedHook", - // )); - // civicrm_api3('Setting', 'create', array( - // 'myWeirdFieldSetting' => array('id' => $customFieldId, 'weirdness' => 1), - // )); - // } - - /** - * Example: Run an external SQL script when the module is uninstalled. - * - * Note that if a file is present sql\auto_uninstall that will run regardless of this hook. - */ - // public function uninstall(): void { - // $this->executeSqlFile('sql/my_uninstall.sql'); - // } - - /** - * Example: Run a simple query when a module is enabled. - */ - // public function enable(): void { - // CRM_Core_DAO::executeQuery('UPDATE foo SET is_active = 1 WHERE bar = "whiz"'); - // } - - /** - * Example: Run a simple query when a module is disabled. - */ - // public function disable(): void { - // CRM_Core_DAO::executeQuery('UPDATE foo SET is_active = 0 WHERE bar = "whiz"'); - // } - - /** - * Example: Run a couple simple queries. - * - * @return TRUE on success - * @throws CRM_Core_Exception - */ - // public function upgrade_4200(): bool { - // $this->ctx->log->info('Applying update 4200'); - // CRM_Core_DAO::executeQuery('UPDATE foo SET bar = "whiz"'); - // CRM_Core_DAO::executeQuery('DELETE FROM bang WHERE willy = wonka(2)'); - // return TRUE; - // } - - /** - * Example: Run an external SQL script. - * - * @return TRUE on success - * @throws CRM_Core_Exception - */ - // public function upgrade_4201(): bool { - // $this->ctx->log->info('Applying update 4201'); - // // this path is relative to the extension base dir - // $this->executeSqlFile('sql/upgrade_4201.sql'); - // return TRUE; - // } - - /** - * Example: Run a slow upgrade process by breaking it up into smaller chunk. - * - * @return TRUE on success - * @throws CRM_Core_Exception - */ - // public function upgrade_4202(): bool { - // $this->ctx->log->info('Planning update 4202'); // PEAR Log interface - - // $this->addTask(E::ts('Process first step'), 'processPart1', $arg1, $arg2); - // $this->addTask(E::ts('Process second step'), 'processPart2', $arg3, $arg4); - // $this->addTask(E::ts('Process second step'), 'processPart3', $arg5); - // return TRUE; - // } - // public function processPart1($arg1, $arg2) { sleep(10); return TRUE; } - // public function processPart2($arg3, $arg4) { sleep(10); return TRUE; } - // public function processPart3($arg5) { sleep(10); return TRUE; } - - /** - * Example: Run an upgrade with a query that touches many (potentially - * millions) of records by breaking it up into smaller chunks. - * - * @return TRUE on success - * @throws CRM_Core_Exception - */ - // public function upgrade_4203(): bool { - // $this->ctx->log->info('Planning update 4203'); // PEAR Log interface - - // $minId = CRM_Core_DAO::singleValueQuery('SELECT coalesce(min(id),0) FROM civicrm_contribution'); - // $maxId = CRM_Core_DAO::singleValueQuery('SELECT coalesce(max(id),0) FROM civicrm_contribution'); - // for ($startId = $minId; $startId <= $maxId; $startId += self::BATCH_SIZE) { - // $endId = $startId + self::BATCH_SIZE - 1; - // $title = E::ts('Upgrade Batch (%1 => %2)', array( - // 1 => $startId, - // 2 => $endId, - // )); - // $sql = ' - // UPDATE civicrm_contribution SET foobar = apple(banana()+durian) - // WHERE id BETWEEN %1 and %2 - // '; - // $params = array( - // 1 => array($startId, 'Integer'), - // 2 => array($endId, 'Integer'), - // ); - // $this->addTask($title, 'executeSql', $sql, $params); - // } - // return TRUE; - // } + public function upgrade_1000(): bool { + E::schema()->createEntityTable('schema/upgrader/1000-RiverleaStream.entityType.php'); + return TRUE; + } } diff --git a/ext/riverlea/schema/upgrader/1000-RiverleaStream.entityType.php b/ext/riverlea/schema/upgrader/1000-RiverleaStream.entityType.php new file mode 100644 index 000000000000..12b5c2fe2489 --- /dev/null +++ b/ext/riverlea/schema/upgrader/1000-RiverleaStream.entityType.php @@ -0,0 +1,137 @@ + 'RiverleaStream', + 'table' => 'civicrm_riverlea_stream', + 'class' => 'CRM_riverlea_DAO_RiverleaStream', + 'getInfo' => fn() => [ + 'title' => E::ts('RiverleaStream'), + 'title_plural' => E::ts('RiverleaStreams'), + 'description' => E::ts('Streams are configurable themes in the Riverlea Theme Framework'), + 'log' => TRUE, + ], + 'getIndices' => fn() => [ + 'index_stream_name' => [ + 'fields' => [ + 'name' => TRUE, + ], + 'unique' => TRUE, + 'add' => '6.1', + ], + ], + 'getPaths' => fn() => [ + // 'add' => 'civicrm/admin/riverlea/stream/create', + // 'update' => 'civicrm/admin/riverlea/stream/update#?RiverleaStream=[id]', + // 'delete' => 'civicrm/contact/view/delete?reset=1&delete=1&cid=[id]', + ], + 'getFields' => fn() => [ + 'id' => [ + 'title' => E::ts('ID'), + 'sql_type' => 'int unsigned', + 'input_type' => 'Number', + 'required' => TRUE, + 'description' => E::ts('Unique RiverleaStream ID'), + 'primary_key' => TRUE, + 'auto_increment' => TRUE, + ], + 'name' => [ + 'title' => ts('Machine-Name'), + 'sql_type' => 'varchar(255)', + 'input_type' => 'Text', + 'description' => ts('Machine-name for this stream.'), + ], + 'label' => [ + 'title' => ts('Label'), + 'sql_type' => 'varchar(512)', + 'input_type' => 'Text', + 'required' => TRUE, + 'description' => ts('User-facing name for this stream'), + ], + 'description' => [ + 'title' => E::ts('Description'), + 'sql_type' => 'text', + 'input_type' => 'TextArea', + 'description' => E::ts('Description of this stream'), + 'default' => NULL, + ], + 'is_reserved' => [ + 'title' => E::ts('Is Reserved?'), + 'description' => E::ts('Reserved streams are not editable through the UI'), + 'sql_type' => 'boolean', + 'input_type' => 'CheckBox', + 'required' => TRUE, + 'default' => FALSE, + ], + 'extension' => [ + 'title' => ts('Extension'), + 'sql_type' => 'varchar(255)', + 'input_type' => 'Select', + 'description' => ts('Extension that provides this stream.'), + 'default' => NULL, + // @todo why not work? + // 'entity_reference' => [ + // 'entity' => 'Extension', + // 'key' => 'file', + // 'on_delete' => 'CASCADE', + // ], + ], + 'file_prefix' => [ + 'title' => ts('Extension File Prefix'), + 'sql_type' => 'varchar(255)', + 'input_type' => 'Text', + 'description' => ts('File prefix to stream files within extension'), + 'default' => NULL, + ], + 'css_file' => [ + 'title' => ts('CSS File'), + 'description' => ts('A file containing stream css - path should be relative to the extension and file_prefix'), + 'sql_type' => 'varchar(512)', + 'input_type' => 'Text', + 'default' => NULL, + ], + 'css_file_dark' => [ + 'title' => ts('Dark-mode CSS File'), + 'description' => ts('A file containing stream css for darkmode - path should be relative to the extension and file_prefix'), + 'sql_type' => 'varchar(512)', + 'input_type' => 'Text', + 'default' => NULL, + ], + 'vars' => [ + 'title' => E::ts('Variable Settings'), + 'sql_type' => 'text', + 'description' => E::ts('Variable declarations for this stream'), + 'default' => NULL, + 'serialize' => CRM_Core_DAO::SERIALIZE_JSON, + ], + 'vars_dark' => [ + 'title' => E::ts('Dark-mode Variable Settings'), + 'sql_type' => 'text', + 'description' => E::ts('Variable declaration overrides for the dark mode of this stream'), + 'default' => NULL, + 'serialize' => CRM_Core_DAO::SERIALIZE_JSON, + ], + 'custom_css' => [ + 'title' => E::ts('Custom CSS'), + 'sql_type' => 'text', + 'input_type' => 'TextArea', + 'description' => E::ts('Custom css for this stream'), + 'default' => NULL, + ], + 'custom_css_dark' => [ + 'title' => E::ts('Dark-mode Custom CSS'), + 'sql_type' => 'text', + 'input_type' => 'TextArea', + 'description' => E::ts('Custom css for the darkmode of this stream'), + 'default' => NULL, + ], + 'modified_date' => [ + 'title' => ts('Modified Date'), + 'sql_type' => 'timestamp', + 'input_type' => 'Select Date', + 'readonly' => TRUE, + 'description' => ts('When the stream was last modified - helps with cache busting.'), + 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', + ], + ], +]; From 12a33d5fdc297965ff309fc82c1ed4202317f568 Mon Sep 17 00:00:00 2001 From: benjamin Date: Wed, 11 Jun 2025 16:39:26 +0100 Subject: [PATCH 6/8] riverlea - update docs for creating a new stream --- ext/riverlea/README.md | 102 ++++++------------ .../RiverleaStream_MyStream.mgd.php.template | 45 ++++++++ 2 files changed, 80 insertions(+), 67 deletions(-) create mode 100644 ext/riverlea/docs/RiverleaStream_MyStream.mgd.php.template diff --git a/ext/riverlea/README.md b/ext/riverlea/README.md index d109bf61eabb..06bdeb85ef6d 100755 --- a/ext/riverlea/README.md +++ b/ext/riverlea/README.md @@ -1,15 +1,16 @@ # RiverLea Theme Framework -This Framework separates CiviCRM's visual/UI CSS from structural CSS, using CSS variables. Installing it provides you with four subthemes or 'Streams' which are entirely created with CSS variables (other than Thames, which uses a little bit of CSS as well): +This Framework separates CiviCRM's visual/UI CSS from structural CSS, using CSS variables. Installing it provides you with four new Riverlea Themes or "Streams" which are almost entirely created with CSS variables: - Minetta, named after the river that runs under Greenwich, NYC. It is based on Civi's default 'Greenwich' theme. - Walbrook, named after the river that runs under Shoreditch, London. It is based on Shoreditch/TheIsland theme. - Hackney, named after the river that runs under Finsbury Park, based on Finsbury Park theme. - Thames, named after the river that runs close to Artful Robot HQ, based on their Aah theme. - You can chose between these subthemes via Display Settings, where you can also set dark-mode preferences. - The extension is licensed under [AGPL-3.0](LICENSE.txt). +When you enable CiviCRM, you will see these themes in your usual CiviCRM theme options on the Display Settings page. - ## Use in Front-End CiviCRM +The extension is licensed under [AGPL-3.0](LICENSE.txt). + +## Use in Front-End CiviCRM **USE WITH CAUTION AND TESTING** While RiverLea has been widely tested in the backend of CiviCRM, be very careful to use in front-end. Given the wide number of themes and scenarios for front-end pages, for existing sites we recommend only applying it to an existing web front-end after comprehensive testing on a dev site. @@ -42,40 +43,23 @@ Please ignore previous numbering patterns. ## Installation -### With (CLI, Zip) - -Sysadmins and developers may download the `.zip` file for this extension and -install it with the command-line tool [cv](https://github.com/civicrm/cv). - -```bash -cd -wget https://lab.civicrm.org/extensions/riverlea/-/archive/main/riverlea-main.zip -unzip riverlea-main.zip -``` - -### With (CLI, Git) - -Sysadmins and developers may clone the [Git](https://en.wikipedia.org/wiki/Git) repo for this extension and -install it with the command-line tool [cv](https://github.com/civicrm/cv). - -```bash -git clone https://lab.civicrm.org/extensions/riverlea.git -cv en riverlea -``` +This extension is bundled with CiviCRM core from version 5.82 onwards. You can install it from the Manage Extensions page. ### After installation -After installing the extension, go to Nav menu > Administer > Customize Data and Screens > Display Preferences, and select which subtheme/stream you want. +After installing the extension, go to Nav menu > Administer > Customize Data and Screens > Display Preferences, and select the stream you want. + +You can also set your Dark Mode preference on this screen: either always use Light Mode, always use Dark Mode, or let the user's browser/OS decide. ## Extension Structure -### Core variables -A list of all base variables used on all streams is at `core/css/_variables.css`. +### Core directory - `core` -### Stream/subtheme directories -Each ‘stream’ or subtheme directory must contain a further directory `css` with a `_variables.css`file and a `_dark.css` if darkmode is supported. Variables in this `_variables.css` file will overrule any variables in the core list above. The subtheme can also include fonts, images and other CSS files, which can be loaded from the `_variables.css` file as an import. +The majority of the Riverlea extension is a layer of core CSS, which styles CiviCRM markup based on the value +of a number of CSS variables. + +The list of all CSS variables with their default values can be found at `core/css/_variables.css`. -### Core directory Contains CSS files in: - In the **core/css** directory are theme files marked with an underscore: - core/css/_base.css – resets, basic type, colours, links, positioning @@ -89,6 +73,13 @@ Contains CSS files in: - other files here without underscore (`admin.css`, `api4-explorer.css`, `contactSummary.css` etc) overrides civicrm's CSS core directory with files of the same name that are called by templates and only load in certain parts of Civi. E.g. `dashboard.css` loads on the CiviCRM main dashboard, and no-where else. - three directories: `org.civicrm.afform-ang` for Afform output, `org.civicrm.afform_admin-ang` for FormBuilder and `org.civicrm.search_kit-css` for SearchKit replace css files in core Civi extensions. +### Stream directories - `streams/[stream_name]` +Each stream in the extension has a subdirectory under `streams` which primarily contains CSS files for the stream: a `_variables.css`file and a `_dark.css` if darkmode is supported. Variables in this `_variables.css` file will overrule any variables in the core list above. + +A stream could also include fonts, images and other CSS files, which can be loaded from the `_variables.css` file as an import. + +Each stream also has a Managed Record file which is used to "install" the stream. These are found in the `managed` directory. + ## Customising Adding a customiser is on the roadmap, with a working prototype, but until its issues are resolved customising can be done through one of the three following methods: @@ -103,53 +94,30 @@ For instance, to give all contribution page buttons rounded corners, you could a Exploring the _variables.css file will give you idea of how much can be overwritten. -### 2. Create a subtheme 'stream' +### 2. Create a new stream -1. Inside the `/streams/` directory is an example stream called `empty`. Duplicate this and rename it the name of your stream. -2. In riverlea.php add a theme array to the function `riverlea_civicrm_themes(&$themes)`. -3. Edit `/streams/[streamname]/css/_variables.css` with your custom css variables. You can link to other CSS files, fonts or images in this file - inside the stream. +Currently the best way to create a new stream is by adding a managed record to an extension. -E.g. to add a stream called "Vimur", you would name the directory 'vimur', and add the following: +1. Enable the `mgd-php` mixin in your extensions `info.xml` ``` - function riverlea_civicrm_themes(&$themes) { - $themes['vimur'] = array( - 'ext' => 'riverlea', - 'title' => 'Riverlea: Vimur', - 'prefix' => 'streams/vimur/', - ); - $themes['minetta'] = array( - 'ext' => 'riverlea', - 'title' => 'Riverlea: Minetta (~Greenwich)', - 'prefix' => 'streams/minetta/', - ); - … - } + + menu-xml@1.0.0 + mgd-php@1.0.0 + ... + ``` -Use of the [ThemeTest extension](https://lab.civicrm.org/extensions/themetest) is recommended to more quickly identify which CSS variables match which UI element, and test multiple variations for each. - -IMPORTANT NOTE: Every time you upgrade RiverLea you will need to add your Stream again. This is obviously less than ideal, so for produciton you may prefer option 3: +2. Copy the template `*.mgd.php` from the `docs` folder of this extension into your extension's `managed` folder (create the managed folder if it doesn't exist). Remove the `.template` part so the file extension is `*.mgd.php`. -### 3. Create a subtheme extension +3. Update the `use CRM_riverlea_ExtensionUtil` line for the equivalent from your extension. -NB: this approach has had very limited testing +4. You can add variable declarations directly to the `vars` and `vars_dark` keys in the `*.mgd.php` file. -1. Create a theme extension using Civix, following the [instructions in the CiviCRM Developer Guide](https://docs.civicrm.org/dev/en/latest/framework/theme/). -2. Create a subtheme of RiverLea using the instructions in **2. Create a subtheme 'stream'** above. -3. Copy the subtheme into the root of your new theme extenion. -4. Edit its main php file, enable both extensions and select your stream. -E.g. for a stream called 'styx', with a theme extension called 'ocean', then in ocean.php you would write: +5. Or you can create `stream/main.css` and `stream/dark.css` files in your extension, and add your CSS to them. It's best to use variable declarations as much as possible to maintain compatibility with future versions +of CiviCRM. -``` -function ocean_civicrm_themes(&$themes) { - $themes['styx'] = array( - 'ext' => 'ocean', - 'title' => 'River Styx', - 'prefix' => 'styx/', - 'search_order' => array('_riverlea_core_', 'styx', '_fallback_'), - ); -} +Use of the [ThemeTest extension](https://lab.civicrm.org/extensions/themetest) is recommended to more quickly identify which CSS variables match which UI element, and test multiple variations for each. ## Troubleshooting - Unless you really need it (e.g. applying an urgent fix, or running a test), delete the custom/ext version of RiverLea, once you are on CiviCRM 5.80 or later. diff --git a/ext/riverlea/docs/RiverleaStream_MyStream.mgd.php.template b/ext/riverlea/docs/RiverleaStream_MyStream.mgd.php.template new file mode 100644 index 000000000000..d58db63425d2 --- /dev/null +++ b/ext/riverlea/docs/RiverleaStream_MyStream.mgd.php.template @@ -0,0 +1,45 @@ + 'RiverleaStream_MyStream', +// 'entity' => 'RiverleaStream', +// 'update' => 'always', +// 'cleanup' => 'always', +// 'params' => [ +// 'version' => 4, +// 'values' => [ +// +// # Describe your stream: +// +// 'name' => 'my_unique_stream_name', +// 'label' => E::ts('My Stream'), +// 'description' => 'Describe your stream...', +// +// # Set stream variables directly here: +// +// 'vars' => [ +// '--crm-c-primary' => 'purple', +// ], +// 'vars_dark' => [ +// # Extra variables for dark mode +// '--crm-c-primary' => 'blue', +// ], +// +// # If you want to add static css files to your stream, then uncomment +// # the following four lines, and create your files at +// # - 'your_extension/stream/main.css' +// # - 'your_extension/stream/dark.css' +// +// 'extension' => 'my_extension_name', # or E::SHORT_NAME if you update the extensionutils above +// 'file_prefix' => 'stream/', +// 'css_file' => 'main.css', +// 'css_file_dark' => 'dark.css', +// ], +// 'match' => ['name'], +// ], +// ], +// ]; From e9b567eb8e18d51f6247ed921e982822d5db6900 Mon Sep 17 00:00:00 2001 From: benjamin Date: Wed, 11 Jun 2025 16:52:52 +0100 Subject: [PATCH 7/8] riverlea - update managed streams for clearer file paths --- ext/riverlea/managed/RiverleaStream_HackneyBrook.mgd.php | 8 ++++---- ext/riverlea/managed/RiverleaStream_Minetta.mgd.php | 8 ++++---- ext/riverlea/managed/RiverleaStream_Thames.mgd.php | 8 ++++---- ext/riverlea/managed/RiverleaStream_Walbrook.mgd.php | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ext/riverlea/managed/RiverleaStream_HackneyBrook.mgd.php b/ext/riverlea/managed/RiverleaStream_HackneyBrook.mgd.php index b492b4c08cdf..ebfb99f2eb53 100644 --- a/ext/riverlea/managed/RiverleaStream_HackneyBrook.mgd.php +++ b/ext/riverlea/managed/RiverleaStream_HackneyBrook.mgd.php @@ -15,10 +15,10 @@ 'label' => E::ts('Hackney Brook'), 'description' => 'named after the Hackney Brook, a tributary of the River Lea that ran through Finsbury Park', 'is_reserved' => TRUE, - 'extension' => 'riverlea', - 'file_prefix' => 'streams/hackneybrook/', - 'css_file' => 'css/_variables.css', - 'css_file_dark' => 'css/_dark.css', + 'extension' => E::SHORT_NAME, + 'file_prefix' => 'streams/hackneybrook/css/', + 'css_file' => '_variables.css', + 'css_file_dark' => '_dark.css', 'vars' => [], 'vars_dark' => [], ], diff --git a/ext/riverlea/managed/RiverleaStream_Minetta.mgd.php b/ext/riverlea/managed/RiverleaStream_Minetta.mgd.php index b2bd3675ce8f..f898a559dde1 100644 --- a/ext/riverlea/managed/RiverleaStream_Minetta.mgd.php +++ b/ext/riverlea/managed/RiverleaStream_Minetta.mgd.php @@ -15,10 +15,10 @@ 'description' => 'Generic CiviCRM UI, somewhat familiar to users of CiviCRM since 2014. Named after Minetta Creek, which runs under Greenwich, New York', 'label' => E::ts('Minetta'), 'is_reserved' => TRUE, - 'extension' => 'riverlea', - 'file_prefix' => 'streams/minetta/', - 'css_file' => 'css/_variables.css', - 'css_file_dark' => 'css/_dark.css', + 'extension' => E::SHORT_NAME, + 'file_prefix' => 'streams/minetta/css/', + 'css_file' => '_variables.css', + 'css_file_dark' => '_dark.css', 'vars' => [ '--crm-version' => "'Minetta, v' var(--crm-release)", ], diff --git a/ext/riverlea/managed/RiverleaStream_Thames.mgd.php b/ext/riverlea/managed/RiverleaStream_Thames.mgd.php index d6fe5f452abc..403a6b2d41bc 100644 --- a/ext/riverlea/managed/RiverleaStream_Thames.mgd.php +++ b/ext/riverlea/managed/RiverleaStream_Thames.mgd.php @@ -15,10 +15,10 @@ 'description' => 'Aaaaah', 'label' => E::ts('Thames'), 'is_reserved' => TRUE, - 'extension' => 'riverlea', - 'file_prefix' => 'streams/thames/', - 'css_file' => 'css/_variables.css', - 'css_file_dark' => 'css/_dark.css', + 'extension' => E::SHORT_NAME, + 'file_prefix' => 'streams/thames/css/', + 'css_file' => '_variables.css', + 'css_file_dark' => '_dark.css', 'vars' => [], ], 'match' => ['name'], diff --git a/ext/riverlea/managed/RiverleaStream_Walbrook.mgd.php b/ext/riverlea/managed/RiverleaStream_Walbrook.mgd.php index 4924a3c495bb..67027d3b8c13 100644 --- a/ext/riverlea/managed/RiverleaStream_Walbrook.mgd.php +++ b/ext/riverlea/managed/RiverleaStream_Walbrook.mgd.php @@ -15,10 +15,10 @@ 'description' => 'Based on Shoreditch theme. Named after after River Walbrook, which runs under Shoreditch, London', 'label' => E::ts('Walbrook'), 'is_reserved' => TRUE, - 'extension' => 'riverlea', - 'file_prefix' => 'streams/walbrook/', - 'css_file' => 'css/_variables.css', - 'css_file_dark' => 'css/_dark.css', + 'extension' => E::SHORT_NAME, + 'file_prefix' => 'streams/walbrook/css/', + 'css_file' => '_variables.css', + 'css_file_dark' => '_dark.css', 'vars' => [], ], 'match' => ['name'], From c1faa910b2f4dc189506e41d3aa1fcd3daa8da75 Mon Sep 17 00:00:00 2001 From: benjamin Date: Mon, 16 Jun 2025 16:20:35 +0100 Subject: [PATCH 8/8] riverlea - add note that it's early days for overridding vars; link to extension docs --- ext/riverlea/README.md | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/ext/riverlea/README.md b/ext/riverlea/README.md index 06bdeb85ef6d..0756f07b95b3 100755 --- a/ext/riverlea/README.md +++ b/ext/riverlea/README.md @@ -82,7 +82,15 @@ Each stream also has a Managed Record file which is used to "install" the stream ## Customising -Adding a customiser is on the roadmap, with a working prototype, but until its issues are resolved customising can be done through one of the three following methods: +Riverlea is designed to allow users to customise their UI by tweaking variables or creating new streams. + +At this stage, small changes are still being made to the variable structure, so any customisations you make may require revisiting in a future upgrade. + +The `core/css/_variables.css` file will give you idea of variables which can be altered. + +A [Customiser tool](https://github.com/civicrm/civicrm-core/pull/32344) is also being developed to create and edit Streams more easily through the UI. + +Use of the [ThemeTest extension](https://lab.civicrm.org/extensions/themetest) is recommended to more quickly identify which CSS variables match which UI element, and test multiple variations for each. ### 1. Add CSS variables to your parent theme @@ -92,11 +100,31 @@ For instance, to give all contribution page buttons rounded corners, you could a --crm-btn-radius: 2rem; ``` -Exploring the _variables.css file will give you idea of how much can be overwritten. +### 2. Add a custom CSS snippet in an extension + +You can override variables by listening to `hook_civicrm_alterBundle`: + +``` +function my_ext_civicrm_alterBundle(CRM_Core_Resources_Bundle $bundle) { + if ($bundle->name === 'coreResources') { + $riverleaOverrides = <<addStyle($riverleaOverrides, ['weight' => 200]); + } +} +``` + +### 3. Create a new stream (preview) + +New streams can be created using CiviCRM's Managed Entity system. (This is the same mechanism as can be used for packaging SearchKits, CustomFields, ContactTypes etc). -### 2. Create a new stream +The Customiser will provide tools to automate some of this process. -Currently the best way to create a new stream is by adding a managed record to an extension. +0. If not adding to an existing extension, [follow the docs to create one](https://docs.civicrm.org/dev/en/latest/extensions/civix/#generate-module). 1. Enable the `mgd-php` mixin in your extensions `info.xml` @@ -117,7 +145,6 @@ Currently the best way to create a new stream is by adding a managed record to a 5. Or you can create `stream/main.css` and `stream/dark.css` files in your extension, and add your CSS to them. It's best to use variable declarations as much as possible to maintain compatibility with future versions of CiviCRM. -Use of the [ThemeTest extension](https://lab.civicrm.org/extensions/themetest) is recommended to more quickly identify which CSS variables match which UI element, and test multiple variations for each. ## Troubleshooting - Unless you really need it (e.g. applying an urgent fix, or running a test), delete the custom/ext version of RiverLea, once you are on CiviCRM 5.80 or later.