diff --git a/FEATURES/140057 b/FEATURES/140057
new file mode 100644
index 0000000000000000000000000000000000000000..610f37e367651f175baca14dd4db6ffc894602fb
--- /dev/null
+++ b/FEATURES/140057
@@ -0,0 +1,10 @@
+        '140057' =>
+            ['Label' => $this->_('Administration des domaines par site'),
+             'Desc' => $this->_('Permet (sur option) d\'affecter une arboresence de domaines à un site voulu. Les permissions des rédacteurs / administrateurs bibliothèque sont prises en compte'),
+             'Image' => '',
+             'Video' => 'https://youtu.be/dIfvaOrHkq0',
+             'Category' => '',
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => 'https://wiki.bokeh-library-portal.org/index.php/Domaines',
+             'Test' => '',
+             'Date' => '2021-08-24'],
\ No newline at end of file
diff --git a/VERSIONS_WIP/140057 b/VERSIONS_WIP/140057
new file mode 100644
index 0000000000000000000000000000000000000000..62001786cf96b7d09381add051ec95a1c8610207
--- /dev/null
+++ b/VERSIONS_WIP/140057
@@ -0,0 +1 @@
+ - ticket #140057 : Administration des domaines par site (à l'instar des articles)
\ No newline at end of file
diff --git a/application/modules/admin/controllers/CatalogueController.php b/application/modules/admin/controllers/CatalogueController.php
index 35904dbac5375bcd1fbfbdc16ce8692a474b4183..d9391e33ca50138c490457ae35a8c70bb208ab00 100644
--- a/application/modules/admin/controllers/CatalogueController.php
+++ b/application/modules/admin/controllers/CatalogueController.php
@@ -23,6 +23,10 @@
 class Admin_CatalogueController extends ZendAfi_Controller_Action {
   protected $_user;
 
+  public function getPlugins() {
+    return [ZendAfi_Controller_Plugin_Manager_Catalogue::class];
+  }
+
   public function preDispatch() {
     parent::preDispatch();
     $this->view->user = $this->_user = Class_Users::getIdentity();
@@ -30,9 +34,56 @@ class Admin_CatalogueController extends ZendAfi_Controller_Action {
 
 
   public function indexAction() {
+    $this->view->titre = $this->_('Définition des domaines');
+
+    if (Class_AdminVar::isModuleEnabled('ENABLE_DOMAINS_PER_LIBRARIES'))
+        return $this->_renderListMode();
+
     if (!$this->view->catalogues = Class_Catalogue::findTopCatalogues())
       $this->view->message = $this->_('Aucun domaine n\'a été trouvé');
-    $this->view->titre = $this->_('Définition des domaines');
+  }
+
+
+  protected function _findAccessibleLibrariesForDomainsListMode() {
+    $libraries = Class_Bib::findAllAccessibleToCurrentUser();
+    if (($this->_user->hasRightToAccess(Class_UserGroup::RIGHT_USER_DOMAINES_TOTAL_ACCESS)
+         ||
+         $this->_user->hasRightToAccess(Class_UserGroup::RIGHT_USER_DOMAINES_SUPPRESSION_LIMIT)
+         ||
+         !$this->_user->hasUserGroups()) && $this->_user->isRoleLibraryLimited()) {
+      $libraries []= Class_Bib::getPortail();
+    }
+    return $libraries;
+  }
+
+
+  protected function _renderListMode() {
+    if($this->_request->isPost())
+      return $this->_redirectToRefererWithNewParams(['title_search' => $this->_getPost()['title_search'],
+                                                     'order' => $this->_getParam('order')]);
+
+    $library = ($id_bib = $this->_getParam('id_bib', null))
+      ? Class_Bib::find($id_bib)
+      : Class_Bib::findForCurrentUser();
+
+    if (!$domain = Class_Catalogue::find($this->_getParam('id', null)))
+      $domain = Class_Catalogue::newInstanceWithId(null,
+                                                   ['sous_domaines' => [],
+                                                    'library_id' => $library->getId()]);
+
+    $params = ['bib' => $domain->getLibrary(),
+               'id_bib' => $id_bib,
+               'search_value' => trim($this->_getParam('title_search', null)),
+               'bibs' => $this->_findAccessibleLibrariesForDomainsListMode(),
+               'model' => $domain];
+
+    $this->view->list = ($params['search_value']
+                         || !$domain->isNew()
+                         || (null !== $params['id_bib']))
+        ? $this->_helper->ListViewMode_Catalogue($params)
+        : $this->_helper->ListViewMode_Library($params);
+
+    $this->renderScript('admin/listViewMode.phtml');
   }
 
 
@@ -149,6 +200,13 @@ class Admin_CatalogueController extends ZendAfi_Controller_Action {
     $this->view->catalogue = $model;
     $this->view->form = $form;
 
+    if (Class_AdminVar::isModuleEnabled('ENABLE_DOMAINS_PER_LIBRARIES'))
+      $form->setAttrib('data-backurl',
+                       Class_Url::relative(['module' => 'admin',
+                                            'controller' => 'catalogue',
+                                            'id_catalogue' => null,
+                                            'id' => $model->getParentId() ? $model->getParentId() : null,
+                                            'id_bib' => $model->getParentId() ? null : (int)$model->getLibraryId()]));
     return $form->setAction($this->view->url());
   }
 
@@ -164,6 +222,8 @@ class Admin_CatalogueController extends ZendAfi_Controller_Action {
 
     $post = $this->_transformPostValuesToThesaurusValues($post);
     $post = $this->_transformPostValuesToCustomFormValues($post, $catalogue);
+    if ($library_id = $this->_getParam('id_bib'))
+      $post['library_id'] = $library_id;
 
     return $catalogue
       ->updateAttributes($post)
@@ -238,19 +298,13 @@ class Admin_CatalogueController extends ZendAfi_Controller_Action {
 
 
   public function domainesAction() {
-    $data = [(new Class_Catalogue())->getDomainesJsonWithoutPaniers()];
+    $data = (new Class_Catalogue_TreeSelectJson())->renderWithoutCarts();
     $this->_helper->json($data);
   }
 
 
   public function browsableDomainsAction() {
-    $this->_helper->viewRenderer->setNoRender();
-    $data = [];
-    $data[] = (new Class_Catalogue())->getBrowsableDomainsJson();
-    $JSON = json_encode($data);
-
-    $this->getResponse()->setHeader('Content-Type', 'application/json; charset=utf-8');
-    $this->getResponse()->setBody($JSON);
+    return $this->_helper->json((new Class_Catalogue_TreeSelectJson())->renderBrowsableDomains());
   }
 
 
@@ -303,6 +357,7 @@ class Admin_CatalogueController extends ZendAfi_Controller_Action {
     $this->_helper->notify($this->_('Panier "%s" retiré', $panier->getLibelle()));
   }
 
+
   protected function formAjoutPanier($catalogue) {
     $options = ['' => $this->_('Veuiller sélectionner un panier')];
     $paniers = Class_PanierNotice::findAllBelongsToAdmin();
@@ -320,7 +375,7 @@ class Admin_CatalogueController extends ZendAfi_Controller_Action {
 
   public function domainesPaniersJsonAction() {
     $this->_helper->json([$this->_user->getPaniersJson(),
-                          (new Class_Catalogue())->getDomainesJson(),
+                          (new Class_Catalogue_TreeSelectJson())->render(),
                           (new Class_PanierNotice())->getPaniersAdminsNotInCatalogueJson()]);
   }
-}
\ No newline at end of file
+}
diff --git a/application/modules/admin/controllers/CmsController.php b/application/modules/admin/controllers/CmsController.php
index 21274123cc7bdaf5742a97f3c10aabcb9f5e27ab..f5530195aa16a187cb1ace20df704d286ad02683 100644
--- a/application/modules/admin/controllers/CmsController.php
+++ b/application/modules/admin/controllers/CmsController.php
@@ -34,25 +34,13 @@ class Admin_CmsController extends ZendAfi_Controller_Action {
     parent::init();
 
     $this->identity = Class_Users::getIdentity();
-
-    $this->_bib = $this->identity->isRoleLibraryLimited() ?
-      $this->identity->getBib() :
-      $this->_bib = Class_Bib::getPortail();
-  }
-
-
-  protected function _getBibs() {
-    if (0 != $this->_bib->getId())
-      return [$this->_bib];
-
-    $bibs = Class_Bib::findAllBy(['order' => 'libelle']);
-    array_unshift($bibs, $this->_bib);
-    return $bibs;
   }
 
 
   protected function _renderList() {
-    $bibs = $this->_getBibs();
+    $bibs = Class_Bib::findAllAccessibleToCurrentUser();
+    $current_library = Class_Bib::findForCurrentUser();
+
     $ids = array_map(function($model)
                      {
                        return $model->getId();
@@ -65,7 +53,7 @@ class Admin_CmsController extends ZendAfi_Controller_Action {
 
     $id_bib = $this->_getParam('id_bib',
                                $this->identity->isRoleLibraryLimited()
-                               ? $this->_bib->getId()
+                               ? $current_library->getId()
                                : null);
 
     $article = ($id_article = $this->_getParam('id', null))
@@ -80,7 +68,7 @@ class Admin_CmsController extends ZendAfi_Controller_Action {
                'search_value' => $search,
                'order' => $order,
                ZendAfi_Controller_Action_Helper_ListViewMode_Article::STATUS_SEARCH => $status,
-               'bib' => $this->_bib,
+               'bib' => $current_library,
                'id_bib' => $id_bib,
                'id_cat' => $id_cat];
 
@@ -111,7 +99,7 @@ class Admin_CmsController extends ZendAfi_Controller_Action {
     if (Class_AdminVar::isArticlesListMode())
       return $this->_renderList();
 
-    $bibs = $this->_getBibs();
+    $bibs = Class_Bib::findAllAccessibleToCurrentUser();
 
     $add_link_builder = function($bib) {
       $links = [];
diff --git a/application/modules/admin/views/scripts/catalogue/_catalogue_row.phtml b/application/modules/admin/views/scripts/catalogue/_catalogue_row.phtml
index 3b16c8d1c2704ce1c84083b6ebc0b71a155476ef..1c7ce59d8433d1bcd48f2682f85d0c638cc30d2a 100644
--- a/application/modules/admin/views/scripts/catalogue/_catalogue_row.phtml
+++ b/application/modules/admin/views/scripts/catalogue/_catalogue_row.phtml
@@ -1,55 +1,14 @@
-<?php
-$catalog = $this->catalogue;
-$user = Class_Users::getIdentity();
-$editable = $catalog->canBeDeletedOrModifyByUser($user);
-?>
 <li class="<?php echo $this->item_class; ?>">
-  <div><?php echo $this->escape($catalog->getLibelle()); ?></div>
-  <div class="commentaire"></div>
-  <div class="actions">
-    <?php
-    $actions = [
-      ['action' => 'tester', 'icon' => 'test', 'help' => $this->_('Indexer les notices')],
-      ['action' => 'edit', 'icon' => 'edit', 'help' => $this->_('Modifier'), 'display' => $editable],
-      ['action' => 'duplicate', 'icon' => 'copy', 'help' => $this->_('Dupliquer')],
-      ['action' => 'add', 'icon' => 'add_page', 'help' => $this->_('Ajouter un sous-domaine'), 'display' => $editable],
-      ['action' => 'paniers', 'icon' => 'basket', 'help' => $this->_('Paniers'), 'display' => $editable]
-    ];
-
-    $current_skin = Class_Admin_Skin::current();
-    foreach ($actions as $action) {
-      if (isset($action['display']) && !$action['display'])
-        continue;
-
-      echo $this->tagAnchor($this->url(['action' => $action['action'],
-                                        'id_catalogue' => $catalog->getId()]),
-                            $current_skin->renderActionIconOn($action['icon'], $this,
-                                                              ['alt' => $action['help'],
-                                                               'title' => $action['help']]));
-    }
-
-    if ($editable)
-      echo $this->tag('a', $this->boutonIco('type=del'),
-                      ['href' => BASE_URL . '/admin/catalogue/delete/id_catalogue/'.$catalog->getId()]);
-
-    $catalogue_url = $this->url(['module' => 'opac',
-                                 'controller' => 'recherche',
-                                 'action' => 'simple',
-                                 'id_catalogue' => $catalog->getId()],
-                                null, true);
-
-    echo $this->tagPreview($catalogue_url,
-                           $this->_('Visualiser le domaine "%s" dans un nouvel onglet', $catalog->getLibelle()));
-    echo $this->permalink($catalogue_url);
-
-    ?>
-  </div>
-  <ul>
-    <?php
-    echo $this->partialCycle('catalogue/_catalogue_row.phtml',
-                             'catalogue',
-                             $catalog->getSousDomaines(),
-                             ['first', 'second']);
+   <div><?php echo $this->escape($this->catalogue->getLibelle()); ?></div>
+   <div class="commentaire"></div>
+   <?php echo $this->renderPluginsActions($this->catalogue) ?>
+   <ul>
+     <?php
+       echo $this->partialCycle('catalogue/_catalogue_row.phtml',
+                                'catalogue',
+                                $this->catalogue->getSousDomaines(),
+                                ['first', 'second'],
+                                ['plugins' => $this->plugins]);
     ?>
   </ul>
 </li>
diff --git a/application/modules/admin/views/scripts/catalogue/index.phtml b/application/modules/admin/views/scripts/catalogue/index.phtml
index e952257b4e8c04406bd379705b61b6b8e1f75498..c8aff5829d6728dfcffb89279c3e60b98ca07098 100644
--- a/application/modules/admin/views/scripts/catalogue/index.phtml
+++ b/application/modules/admin/views/scripts/catalogue/index.phtml
@@ -16,7 +16,8 @@ if (Class_Users::getIdentity()->hasRightAccessDomaines()) { ?>
     echo $this->partialCycle('catalogue/_catalogue_row.phtml',
                              'catalogue',
                              $this->catalogues,
-                             ['first', 'second']);?>
+                             ['first', 'second'],
+                             ['plugins' => $this->plugins]);?>
   </ul>
   <div class="clear"></div>
 </div>
diff --git a/application/modules/opac/controllers/AbonneController.php b/application/modules/opac/controllers/AbonneController.php
index a881c63aca85ea87c04e0161da67b9c9fbee1451..dad49362518ae64183ffc5e238eff586dedabf7d 100644
--- a/application/modules/opac/controllers/AbonneController.php
+++ b/application/modules/opac/controllers/AbonneController.php
@@ -1066,7 +1066,7 @@ class AbonneController extends ZendAfi_Controller_Action {
     $data[] = $this->_user->getPaniersJson();
 
     if ($this->_user->canAccessBackend())
-      $data[] = (new Class_Catalogue())->getDomainesJson(['removeCheckbox' => true]);
+      $data[] = (new Class_Catalogue_TreeSelectJson())->renderWithoutCheckbox();
 
     $this->_helper->json($data);
   }
diff --git a/application/modules/opac/controllers/BibController.php b/application/modules/opac/controllers/BibController.php
index faa212bfa9bfb6f1c165fce7882a7f63dc472655..0757f026ca80c04e3f5808c5928a7fb785681908 100644
--- a/application/modules/opac/controllers/BibController.php
+++ b/application/modules/opac/controllers/BibController.php
@@ -229,13 +229,6 @@ class BibController extends ZendAfi_Controller_Action {
   }
 
 
-  public function giveInfoBulle($id_bib) {
-    $class_bib = new Class_Bib();
-    $bib = $class_bib->getBibById($id_bib);
-    return(addslashes($bib->LIBELLE));
-  }
-
-
   public function widgetPageAction() {
     $viewRenderer = $this->getHelper('ViewRenderer');
 
@@ -391,4 +384,4 @@ class BibController extends ZendAfi_Controller_Action {
     $this->_helper->notify($this->_('Une erreur s\'est produite.'), ['status' => 'error']);
     return $this->_redirectClose($this->_getReferer());
   }
-}
\ No newline at end of file
+}
diff --git a/cosmogramme/sql/patch/patch_416.php b/cosmogramme/sql/patch/patch_416.php
new file mode 100644
index 0000000000000000000000000000000000000000..36124ababfd8657086a9c4c977d1fa0924eaae72
--- /dev/null
+++ b/cosmogramme/sql/patch/patch_416.php
@@ -0,0 +1,8 @@
+<?php
+$adapter = Zend_Db_Table_Abstract::getDefaultAdapter();
+
+try {
+  $adapter->query('ALTER TABLE `catalogue`'
+                  . ' ADD COLUMN `library_id` int(11) unsigned null default null,'
+                  . ' ADD KEY `library_id` (`library_id`)');
+} catch(Exception $e) {}
diff --git a/library/Class/AdminVar.php b/library/Class/AdminVar.php
index 8f4a8f74a37ef115bfb843d8cd1a437317be3e59..2486dffe33d84390dad8fc111967d5ed48fd05de 100644
--- a/library/Class/AdminVar.php
+++ b/library/Class/AdminVar.php
@@ -494,6 +494,7 @@ Pour vous désabonner de la lettre d\'information, merci de cliquer sur le lien
             'OAI_REPOSITORY_NAME' => Class_AdminVar_Meta::newDefault($this->_('Contenu de la balise "repositoryName" dans la réponse au verb Identify, si vide sera [NOM DU SERVEUR] Oai repository')),
             'OAI_ADMIN_EMAIL' => Class_AdminVar_Meta::newDefault($this->_('Contenu de la balise "adminEmail" dans la réponse au verb Identify, si vide sera tiré de la variable cosmogramme "mail_admin"')),
             'CUSTOM_DOMAIN_FORM' => Class_AdminVar_Meta::newOnOff($this->_('Activation de la personnalisation des formulaires des domaines')),
+            'ENABLE_DOMAINS_PER_LIBRARIES' => Class_AdminVar_Meta::newOnOff($this->_('Activation de la répartition des domaines par bibliothèque dans l\'interface d\'administration')),
     ];
   }
 
diff --git a/library/Class/Bib.php b/library/Class/Bib.php
index 45be88a991e0bbe9603ab48fbe022208541f7f30..c170d7e7de1212199ca839ed7fa626b485348725 100644
--- a/library/Class/Bib.php
+++ b/library/Class/Bib.php
@@ -22,12 +22,6 @@
 require_once dirname(__FILE__)."/CompositeBuilder.php";
 
 
-class BibCSite extends Zend_Db_Table_Abstract {
-  protected $_name = 'bib_c_site';
-}
-
-
-
 class BibLoader extends Storm_Model_Loader {
   use Trait_Translator;
 
@@ -270,10 +264,30 @@ class BibLoader extends Storm_Model_Loader {
                           return $o->getJourSemaine() == $day_of_week;
                         });
   }
+
+
+  public function findAllAccessibleToCurrentUser() {
+    $current_library = $this->findForCurrentUser();
+    if (!$current_library->isPortail())
+      return [$current_library];
+
+    $libraries = Class_Bib::findAllBy(['order' => 'libelle']);
+    array_unshift($libraries, $current_library);
+    return $libraries;
+  }
+
+
+  public function findForCurrentUser() {
+    $current_user = Class_Users::getIdentity();
+    return $current_user->isRoleLibraryLimited()
+      ? $current_user->getBib()
+      : $this->getPortail();
+  }
 }
 
 
 
+
 class Class_Bib extends Storm_Model_Abstract {
   use
     Trait_PermissionTargetable,
@@ -624,21 +638,6 @@ class Class_Bib extends Storm_Model_Abstract {
 
 
 
-  public function getBibById($id_bib)
-  {
-    try
-      {
-        $BibCSite = new BibCSite();
-        $where = $BibCSite->getAdapter()->quoteInto('ID_SITE=?', $id_bib);
-        return $fetch = $BibCSite->fetchRow($where);
-      }catch (Exception $e)
-        {
-          logErrorMessage('Class: Class_Zone; Function: getBibById' . NL . $e->getMessage());
-          return $this->_dataBaseError;
-        }
-  }
-
-
   public function getFilePath() {
     return ($this->getFileName() == $this->getFile()) ?
       $this->getBasePath().$this->getFile() : $this->getFile();
diff --git a/library/Class/Catalogue.php b/library/Class/Catalogue.php
index 24db05655f4377960dd1afbfa5aef043f61ff05d..1f20cbae31e14935932e6300d700f364f9df92f6 100644
--- a/library/Class/Catalogue.php
+++ b/library/Class/Catalogue.php
@@ -20,685 +20,6 @@
  */
 
 
-class CatalogueLoader extends Storm_Model_Loader {
-  use Trait_Translator, Trait_TimeSource;
-
-  const DEFAULT_ITEMS_BY_PAGE = 100;
-
-  protected $_indexable_cache;
-
-  public function loadNoticesFor($catalogue, $itemsByPage = self::DEFAULT_ITEMS_BY_PAGE, $page = 1, $find_all_params = null) {
-    if (null == $catalogue)
-      return [];
-
-    if ('' == ($where = $this->clausesFor($catalogue)))
-      return [];
-
-    if (!is_array($find_all_params))
-      $find_all_params = [];
-
-    if (!isset($find_all_params['limitPage']))
-      $find_all_params['limitPage'] = [$page, $itemsByPage];
-
-    $find_all_params['where'] = $where;
-
-    return Class_Notice::findAllBy($find_all_params);
-  }
-
-
-  public function findAllCataloguesAIndexer() {
-    return array_filter(Class_Catalogue::findAllIndexable(),
-                        function ($catalogue)
-                        {
-                          return '' != Class_Catalogue::clausesFor($catalogue);
-                        });
-  }
-
-
-  public function findAllIndexableNotEmpty() {
-    return array_filter(Class_Catalogue::findAllIndexable(),
-                        function($catalog)
-                        {
-                          return !$catalog->isEmpty();
-                        });
-  }
-
-
-  public function findAllIndexable() {
-    return $this->_indexable_cache
-      ? $this->_indexable_cache
-      : $this->_indexable_cache = Class_Catalogue::findAllBy(['indexer' => true,
-                                                              'order' => 'libelle']);
-  }
-
-
-  public function getDomainsForBreadcrumb($breadcrumb) {
-    $domains_ids = array_filter(explode(';', $breadcrumb));
-    $domains = [];
-    foreach ($domains_ids as $domain_id)
-      $domains[] = Class_Catalogue::find($domain_id);
-
-    return $domains;
-  }
-
-
-  public function getMultiOptionsFacets($ids) {
-    $domain_labels = ['' => $this->_('tous')];
-    if (!$domains = Class_Catalogue::findAllBy(['id_catalogue' => $ids,
-                                                'order' => 'libelle']))
-      return $domain_labels;
-
-    foreach($domains as $domain)
-      if ($th = Class_CodifThesaurus::findThesaurusForCatalogue($domain->getId()))
-        $domain_labels[$th->getFacetteIndex()]= $th->getLibelleFacette();
-
-    return $domain_labels;
-  }
-
-
-  public function updateAllThesaurusForCatalogueChildren($catalogue_id, $thesaurus_id) {
-    $catalogues = Class_Catalogue::findAllBy(['parent_id' => $catalogue_id]);
-
-    foreach ($catalogues as $catalogue) {
-      Class_CodifThesaurus::deleteAllWithIdOrigineAndCode($catalogue->getId(),
-                                                          Class_CodifThesaurus::fixedCodeOf('Domain'));
-      $new_thesaurus = $catalogue->getNewThesaurus();
-      $new_thesaurus_id = Class_CodifThesaurus::findNextThesaurusChildId('catalogue',
-                                                                         $thesaurus_id);
-      $new_thesaurus->setIdThesaurus($new_thesaurus_id);
-      $new_thesaurus->save();
-
-      Class_Catalogue::getLoader()
-        ->updateAllThesaurusForCatalogueChildren($catalogue->getId(), $new_thesaurus_id);
-    }
-  }
-
-
-  public function countNoticesFor($catalogue) {
-    if (!$catalogue)
-      return 0;
-
-    if ('' == ($where = $this->clausesFor($catalogue)))
-      return 0;
-
-    return Class_Notice::countBy(['where' => $where]);
-  }
-
-
-  public function clausesFor($catalogue) {
-    if (!$catalogue)
-      return '';
-
-    $fromUntil = $this->fromUntilClauseFor($catalogue);
-
-    if ($catalogue->isMatchingAllNotices())
-      return $fromUntil ? $fromUntil : '1=1';
-
-    $conditions = array_filter(array_merge($this->_catalogueConditions($catalogue),
-                                           [$fromUntil]));
-
-    return 0 < count($conditions)
-      ? implode(' and ', $conditions)
-      : '';
-  }
-
-
-  protected function _localClausesFor($catalogue, $against='') {
-    $conditions = [];
-
-    if ($facets = $this->facetsClauseFor($catalogue, $against))
-      $conditions[] = $facets;
-
-    if ($docType = $this->docTypeClauseFor($catalogue))
-      $conditions[] = $docType;
-
-    if ($year = $this->yearClauseFor($catalogue))
-      $conditions[] = $year;
-
-    if ($cote = $this->coteClauseFor($catalogue))
-      $conditions[] = $cote;
-
-    if ($new = $this->nouveauteClauseFor($catalogue))
-      $conditions[] = $new;
-
-    return $conditions;
-  }
-
-
-  protected function _noticesLinked($catalogue) {
-    if (!$catalogue || $catalogue->isCustomDomainForm())
-      return '';
-
-    if (!Class_NoticeDomain::getClesNoticesForDomain($catalogue->getId()))
-      return '';
-
-    return (new Class_MoteurRecherche())->prepareFacetteDomainForOrConditions($catalogue->getId());
-  }
-
-
-  /**
-   * @param $catalogue Class_Catalogue
-   * @return string
-   */
-  public function facetsClauseFor($catalogue, $against = '') {
-    $against_ou = '';
-    $facets = ['B' => $catalogue->getBibliotheque(),
-               'S' => $catalogue->getSection(),
-               'G' => $catalogue->getGenre(),
-               'L' => $catalogue->getLangue(),
-               'Y' => $catalogue->getAnnexe(),
-               'E' => $catalogue->getEmplacement(),
-               'H' => $catalogue->getThesaurusNovelty()];
-
-    foreach ($facets as $k => $v)
-      $against .= Class_Catalogue::getSelectionFacette($k, $v);
-
-    $facets = ['A' => $catalogue->getAuteur(),
-               'M' => $catalogue->getMatiere(),
-               'D' => $catalogue->getDewey(),
-               'P' => $catalogue->getPcdm4(),
-               'H' => $catalogue->getThesaurus(),
-               'Z' => $catalogue->getTags(),
-               'F' => $catalogue->getInteret()];
-
-    foreach ($facets as $k => $v)
-      $against_ou .= Class_Catalogue::getSelectionFacette($k,
-                                                          $v,
-                                                          in_array($k,
-                                                                   ['M', 'D', 'P','H']),
-                                                          false);
-
-    if ('' != $against_ou)
-      $against .= ' +(' . $against_ou . ")";
-
-    if ('' == $against)
-      return '';
-
-    return "MATCH(facettes) AGAINST('".$against."' IN BOOLEAN MODE)";
-  }
-
-
-  public function docTypeClauseFor($catalogue) {
-    if (!$docType = $catalogue->getTypeDoc())
-      return '';
-
-    $parts = array_filter(explode(';', $docType));
-    return (1 == count($parts)) ?
-      ('notices.type_doc=\'' . $parts[0]) . '\'' :
-      ('notices.type_doc IN (\'' . implode('\', \'', $parts) .  '\')');
-  }
-
-
-  public function yearClauseFor($catalogue) {
-    $clauses = [];
-    if ($start = $catalogue->getAnneeDebut())
-      $clauses[] = "annee >= '" . $start . "'";
-
-    if($end = $catalogue->getAnneeFin())
-      $clauses[] = "annee <= '" . $end . "'";
-
-    if (0 == count($clauses))
-      return '';
-
-    return implode(' and ', $clauses);
-  }
-
-
-  public function coteClauseFor($catalogue) {
-    $clauses = [];
-    if ($start = $catalogue->getCoteDebut())
-      $clauses[] = "cote >= '" . strtoupper($start) . "'";
-
-    if ($end = $catalogue->getCoteFin())
-      $clauses[] = "cote <= '". strtoupper($end) . "'";
-
-    if (0 == count($clauses))
-      return '';
-
-    return implode(' and ', $clauses);
-  }
-
-
-  public function nouveauteClauseFor($catalogue) {
-    /* Attention : la date de creation corresponds à la date de fin de nouveauté */
-    if (1 != $catalogue->getNouveaute())
-      return '';
-
-    return 'date_creation >= \'' . $this->getCurrentDate() . '\'';
-  }
-
-
-  public function fromUntilClauseFor($catalogue) {
-    $clauses = [];
-    if ($start = $catalogue->getFrom())
-      $clauses[] = "created_at >= '" . $start . "'";
-
-    if($end = $catalogue->getUntil())
-      $clauses[] = "created_at <= '" . $end . "'";
-
-    if (0 == count($clauses))
-      return '';
-
-    $clauses[] = 'created_at is not null';
-
-    return implode(' and ', $clauses);
-  }
-
-
-  /**
-   * @return array  [id => label]
-   */
-  public function allByIdLabel() {
-    return $this->byIdLabel(Class_Catalogue::findAllBy(['order' => 'libelle']));
-  }
-
-
-  /**
-   * @return array  [id => label]
-   */
-  public function byIdLabel($domains) {
-    $datas = [];
-    foreach($domains as $domain)
-      $datas[$domain->getId()] = $domain->getLibelle();
-    return $datas;
-  }
-
-
-  /**
-   * @return array
-   */
-  public function findTopCatalogues() {
-    return Class_Catalogue::findAllBy(['where' => 'parent_id is null',
-                                       'order' => 'libelle']);
-  }
-
-
-  /**
-   * @return Class_Catalogue
-   */
-  public function getRoot() {
-    return Class_Catalogue::newInstanceWithId(null,
-                                              ['sous_domaines' => Class_Catalogue::findTopCatalogues()]);
-  }
-
-
-  /**
-   * @param $treeNode Trait_TreeNode
-   * @return Class_Catalogue
-   */
-  public function findWithSamePathAs($treeNode) {
-    return Class_Catalogue::getRoot()->findByPath($treeNode->getPath());
-  }
-
-
-  public function fetchAllNoticesByPreferences($preferences) {
-    $requetes = $this->getRequetes($preferences);
-    if (!array_key_exists("req_liste", $requetes))
-      return [];
-
-    $nb_par_page = (array_key_exists('aleatoire', $preferences)
-                    && array_key_exists('nb_analyse', $preferences)
-                    && $preferences['aleatoire']
-                    && $preferences['nb_analyse'])
-      ? $preferences['nb_analyse']
-      : $preferences['nb_notices'];
-
-    return Class_Notice::findAllByRequeteRecherche($requetes['req_ids'],
-                                                   $nb_par_page,
-                                                   1);
-  }
-
-
-  public function getRequetes($preferences, $fields = null) {
-    if (isset($preferences['id_panier'])
-        && (0 !== (int)$preferences['id_panier']))
-      return $this->getRequetesPanier($preferences);
-
-    $catalogue = isset($preferences['id_catalogue'])
-      ? Class_Catalogue::find($preferences['id_catalogue'])
-      : null;
-
-    if ($catalogue && $catalogue->isEmpty())
-      return [];
-
-    $against = $this->selectionFacettesForCatalogueRequestByPreferences($preferences);
-
-    $conditions = $this->_catalogueConditions($catalogue, $against);
-
-    if (isset($preferences['only_img']) && ($preferences['only_img'] == Class_Catalogue::ONLY_IMG)
-        && (($catalogue && Class_Catalogue::hasFilters($preferences['id_catalogue']))
-            || !$catalogue))
-      $conditions[] = "url_vignette > '' and url_vignette != '" . Class_WebService_Vignette::NO_DATA . "' ";
-
-    if (isset($preferences['only_img']) && ($preferences['only_img'] == Class_Catalogue::WITHOUT_IMG))
-      $conditions[] = "url_vignette=''";
-
-    $join = (isset($preferences['avec_avis']) && ($preferences['avec_avis'] == 1))
-      ? ' INNER JOIN notices_avis ON notices.clef_oeuvre=notices_avis.clef_oeuvre '
-      : '';
-
-    $order_by = $this->orderByForCatalogueRequestByPreferences($preferences);
-    $limite = $this->limitForCatalogueRequestByPreferences($preferences);
-
-    $sql = 'select %s from notices' .
-      $join .
-      (new Class_MoteurRecherche)->getConditionsForRequest(array_filter($conditions),
-                                                           $this->_noticesLinked($catalogue));
-
-    return ['req_liste' => sprintf($sql, $fields ? implode(',', $fields) : '*')
-            . $order_by . $limite,
-            'req_comptage' => sprintf($sql, 'count(*)'),
-            'req_facettes' => sprintf($sql, 'notices.id_notice, notices.type_doc, facettes')
-            . $limite,
-            'req_ids' => sprintf($sql, 'notices.id_notice') . $order_by . $limite];
-  }
-
-
-  protected function _catalogueConditions($catalogue, $against='') {
-    if (!$catalogue)
-      return $against
-        ? ["MATCH(facettes) AGAINST('" . $against . "' IN BOOLEAN MODE)"]
-        : [];
-
-    if (!$catalogue->isCustomDomainForm())
-      return $this->_localClausesFor($catalogue, $against);
-
-    $search_engine = (new Class_MoteurRecherche)
-      ->visitSearchSettings((new Class_CriteresRecherche)
-                            ->clearProfil()
-                            ->setParam('id_catalogue', $catalogue->getId()));
-
-    return !is_array($search_engine->buildWherePartQuery())
-      ? $search_engine->getConditions()
-      : [];
-  }
-
-
-  public function selectionFacettesForCatalogueRequestByPreferences($preferences) {
-    if (!isset($preferences['facettes']))
-      return '';
-
-    $against = '';
-    $facettes = explode(';', $preferences['facettes']);
-    foreach($facettes as $facette) {
-      $facette = trim($facette);
-      $against .= $this->getSelectionFacette(substr($facette, 0, 1), substr($facette, 1));
-    }
-
-    return $against;
-  }
-
-
-  public function orderByForCatalogueRequestByPreferences($preferences) {
-    if( ! array_key_exists('tri', $preferences))
-      return ' order by alpha_titre ';
-
-    $tri = $preferences['tri'];
-
-    if ($tri == '0' || $tri == 'alpha_titre')
-      return ' order by alpha_titre ';
-
-    if ( $tri == '1' || $tri == 'date_creation desc')
-      return ' order by date_creation DESC ';
-
-    if ($tri == '2' || $tri == 'nb_visu desc')
-      return ' order by nb_visu DESC ';
-
-    if ($tri == '3' || $tri == 'annee desc')
-      return ' order by annee DESC ';
-
-    return  $tri instanceof Class_MoteurRecherche_OrderCriteria
-      ? sprintf(' order by %s ', $tri->getOrder())
-      : '';
-  }
-
-
-  public function limitForCatalogueRequestByPreferences($preferences, $max_limited = false) {
-    if ( $max_limited )
-      return ' LIMIT 5000';
-
-    if ( isset($preferences["aleatoire"])
-        && 1 == (int) $preferences["aleatoire"])
-      return sprintf(' LIMIT 0,%d', (int) $preferences["nb_analyse"]);
-
-    if (isset($preferences['nb_notices']) && $preferences["nb_notices"])
-      return sprintf(' LIMIT 0,%d', (int) $preferences["nb_notices"]);
-
-    return ' LIMIT 5000'; //LL: j'ai rajouté une limite max car explosion mémoire sur des catalogues mal définis
-  }
-
-
-  public function getRequetesPanier($preferences) {
-    $panier = null;
-    if (array_key_exists('id_user', $preferences))
-      $panier = Class_PanierNotice::findFirstBy(['id_user' => $preferences['id_user'],
-                                                 'id' => $preferences['id_panier']]);
-    if (!$panier)
-      $panier = Class_PanierNotice::find($preferences['id_panier']);
-
-    if (!$panier)
-      return ['nombre' => 0];
-
-    $cles_notices = $panier->getClesNotices();
-    if (empty($cles_notices))
-      return ['nombre' => 0];
-
-    $keys = [];
-    foreach($cles_notices as $notice) {
-      if (!trim($notice))
-        continue;
-      $keys[] = "'" . $notice . "'";
-    }
-    $in_sql = implode(',', $keys);
-
-    $limite = ($preferences['aleatoire'] == 1)
-      ? $preferences['nb_analyse'] : $preferences['nb_notices'];
-    $limite = ($limite) ? 'LIMIT 0,' . $limite : '';
-
-    $order_by = '';
-
-    if (!isset($preferences["tri"]))
-      $preferences["tri"] = 0;
-
-    if ($preferences["tri"]==0)
-      $order_by=" order by alpha_titre ";
-    if ($preferences["tri"]==1)
-      $order_by=" order by date_creation DESC ";
-    if ($preferences["tri"]==2)
-      $order_by=" order by nb_visu DESC ";
-    if($preferences['tri'] > 2)
-      $order_by = ' order by FIELD(notices.clef_alpha, ' . $in_sql  . ') ';
-
-    $condition = (array_isset("only_img", $preferences)
-                  && $preferences["only_img"] == 1)
-      ? " and url_vignette > '' and url_vignette != '" . Class_WebService_Vignette::NO_DATA . "'"
-      : '';
-    $condition .= ' and type=1 ';
-
-    $join = (array_isset("avec_avis", $preferences) && $preferences["avec_avis"] == 1)
-      ? ' INNER JOIN notices_avis ON notices.clef_oeuvre=notices_avis.clef_oeuvre '
-      : '';
-
-    $sql = 'select %s from notices '
-      . $join
-      . 'where notices.clef_alpha in(' . $in_sql . ')'
-      . $condition;
-
-    return ['req_liste' => sprintf($sql, '*') . $order_by . $limite,
-            'req_comptage' => sprintf($sql, 'count(*)'),
-            'req_facettes' => sprintf($sql, 'id_notice, notices.type_doc, facettes') . $limite,
-            'req_ids' => sprintf($sql, 'notices.id_notice') . $order_by . $limite];
-  }
-
-
-  public function getNoticesFromCacheByPreferences($preferences) {
-    $callback = function() use ($preferences) {
-      return Class_Catalogue::getLoader()->fetchAllNoticesByPreferences($preferences);
-    };
-
-    return (array_key_exists('aleatoire', $preferences)
-            && $preferences['aleatoire'])
-      ? $callback()
-      : (new Storm_Cache())->memoize([$preferences, __CLASS__, __FUNCTION__], $callback);
-  }
-
-
-  public function getNoticesByPreferences($preferences) {
-    if (isset($preferences['id_catalogue'])
-        && ($catalogue = Class_Catalogue::getLoader()->find($preferences['id_catalogue']))) {
-      $preferences['catalogue_cache_key'] = serialize($catalogue->toArray());
-    }
-
-    $notices = $this->getNoticesFromCacheByPreferences($preferences);
-
-    if ((int)$preferences["aleatoire"] !== 1)
-      return $notices;
-
-    shuffle($notices);
-    return array_slice ($notices, 0, $preferences["nb_notices"]);
-  }
-
-
-  public function hasFilters($id) {
-    if(!$catalogue = Class_Catalogue::find($id))
-      return false;
-
-    return !$catalogue->hasNoSettings();
-  }
-
-
-  public function hasFiltersOrLinkedRecords($id) {
-    return ($catalog = Class_Catalogue::find((int)$id))
-      ? !$catalog->isEmpty()
-      : false;
-  }
-
-
-  public function saveThesaurus($catalogue) {
-    if ($thesaurus = Class_CodifThesaurus::findThesaurusForCatalogue($catalogue->getId())) {
-      $catalogue->deleteThesaurusInFacette($thesaurus->getIdThesaurus());
-      $catalogue->updateThesaurusLabel($thesaurus);
-    } else if (!$thesaurus = $catalogue->saveThesauriParents())
-      return;
-
-    if($catalogue->hasDomaineParent()){
-      $parent=$catalogue->getParent();
-      $thesaurus_parent = $parent->saveThesauriParents();
-      if (!$thesaurus_parent)
-        return null;
-      $new_thesaurus_id=Class_CodifThesaurus::findNextThesaurusChildId(
-                                                                       'catalogue',
-                                                                       $thesaurus_parent->getIdThesaurus());
-    }
-    else if (strlen($thesaurus->getIdThesaurus())>8) {
-      $new_thesaurus_id=Class_CodifThesaurus::findNextRacineCatalogue();}
-    else
-      return $thesaurus;
-
-    if ($thesaurus->getId() && ($new_thesaurus_id != $thesaurus->getIdThesaurus())) {
-      $thesaurus->setIdThesaurus($new_thesaurus_id);
-      $thesaurus->setLibelle($catalogue->getLibelle());
-      $thesaurus->save();
-
-      Class_Catalogue::updateAllThesaurusForCatalogueChildren($catalogue->getId(),$new_thesaurus_id);
-      return $thesaurus;
-    }
-
-    $thesaurus->setIdThesaurus($new_thesaurus_id);
-    $thesaurus->setLibelle($catalogue->getLibelle());
-    $thesaurus->save();
-
-    return $thesaurus;
-  }
-
-
-  public function getIds($domains) {
-    return (new Storm_Model_Collection($domains))
-      ->collect('id')
-      ->getArrayCopy();
-  }
-
-
-  public function hasViewableDomain() {
-    return 0 < count(Class_Catalogue::findAllIndexableNotEmpty());
-  }
-
-
-  public function getSelectionFacette($type, $valeurs, $descendants = false, $signe = true) {
-    if ( !$valeurs = array_filter(explode(';', $valeurs)))
-      return false;
-
-    $cond = '';
-    foreach ($valeurs as $valeur) {
-      if (!$valeur)
-        continue;
-
-      if (!$descendants) {
-        $cond .= $type . $valeur . ' ';
-        continue;
-      }
-
-      if ('M' != $type) {
-        $cond .= $type . $valeur . '* ';
-        continue;
-      }
-
-      if (!$matiere = Class_Matiere::find($valeur))
-        continue;
-
-      if ('' != ($sous_vedettes = trim($matiere->getSousVedettes())))
-        $valeur .= str_replace(' ', ' M', ' ' . $sous_vedettes);
-      $cond .= $type . $valeur . ' ';
-    }
-
-    $cond = trim($cond);
-
-    return ($signe) ? ' +(' . $cond . ')' : ' ' . $cond;
-  }
-
-
-  public function newCatalogueForAll() {
-    return new AllNoticesCatalogue();
-  }
-
-
-  public function getCataloguesForCombo()  {
-    if (!$catalogues = Class_Catalogue::findTopCatalogues())
-      return [];
-
-    $liste = [''];
-    foreach($catalogues as $catalogue) {
-      $this->addCataloguePathAndChildrenTo($catalogue, $liste);
-    }
-    return $liste;
-  }
-
-
-  public function addCataloguePathAndChildrenTo($catalogue, &$liste) {
-    $liste[$catalogue->getId()] = implode(' > ', $catalogue->getPathParts());
-    $sous_domaines = $catalogue->getSousDomaines();
-    foreach($sous_domaines as $sous_domaine)
-      $this->addCataloguePathAndChildrenTo($sous_domaine, $liste);
-  }
-
-
-  public function findParentChildrenOf($model) {
-    if ($model->isNew())
-      return array_filter(Class_Catalogue::findTopCatalogues(),
-                          [$this, '_isParent']);
-
-    $children = $model->getSousDomaines();
-    return array_filter($children, [$this, '_isParent']);
-  }
-
-
-  protected function _isParent($model) {
-    return $model->hasSousDomaines();
-  }
-}
-
-
-
 class Class_Catalogue extends Storm_Model_Abstract {
   use Trait_TreeNode, Trait_Translator, Trait_CustomFields, Trait_Facetable;
 
@@ -710,7 +31,7 @@ class Class_Catalogue extends Storm_Model_Abstract {
   protected
     $_table_name = 'catalogue',
     $_table_primary = 'ID_CATALOGUE',
-    $_loader_class = 'CatalogueLoader',
+    $_loader_class = Class_Catalogue_Loader::class,
     $_default_attribute_values = ['parent_id' => null,
                                   'libelle' => '',
                                   'oai_spec' => '',
@@ -737,21 +58,22 @@ class Class_Catalogue extends Storm_Model_Abstract {
                                   'indexer' => 0,
                                   'url_img' => '',
                                   'custom_form_id' => null,
-                                  'custom_form_values' => ''],
+                                  'custom_form_values' => '',
+                                  'library_id' => null],
 
-    $_belongs_to = ['domaine_parent' => ['model' => 'Class_Catalogue',
-                                          'referenced_in' => 'parent_id'],
-                    'user' => ['model' => 'Class_Users',
+    $_belongs_to = ['domaine_parent' => ['model' => Class_Catalogue::class,
+                                         'referenced_in' => 'parent_id'],
+                    'user' => ['model' => Class_Users::class,
                                'referenced_in' => 'id_user'],
-                    'custom_form' => ['model' => 'Class_SearchForm']
+                    'custom_form' => ['model' => Class_SearchForm::class]
     ],
 
-    $_has_many = ['sous_domaines' => ['model' => 'Class_Catalogue',
+    $_has_many = ['sous_domaines' => ['model' => Class_Catalogue::class,
                                       'role' => 'domaine_parent',
                                       'dependents' => 'delete',
                                       'order' => 'libelle'],
 
-                  'panier_notice_catalogues' => ['model' => 'Class_PanierNoticeCatalogue',
+                  'panier_notice_catalogues' => ['model' => Class_PanierNoticeCatalogue::class,
                                                  'dependents' => 'delete',
                                                  'role' => 'catalogue'],
 
@@ -761,7 +83,6 @@ class Class_Catalogue extends Storm_Model_Abstract {
     $_until;
 
 
-  /** [[file:~/public_html/afi-opac3/library/Trait/TreeNode.php::trait%20Trait_TreeNode%20{][voir Trait_TreeNode]] */
   public function getParent() {
     return $this->getDomaineParent();
   }
@@ -1266,6 +587,12 @@ class Class_Catalogue extends Storm_Model_Abstract {
       return true;
     }
 
+    if (Class_AdminVar::isModuleEnabled('ENABLE_DOMAINS_PER_LIBRARIES')
+        && $user->isRoleLibraryLimited()
+        && (!in_array($this->getOwnOrParentLibraryId(), [0, $user->getIdSite()]))) {
+      return false;
+    }
+
     if ($user->hasRightToAccess(Class_UserGroup::RIGHT_USER_DOMAINES_TOTAL_ACCESS))
       return true;
 
@@ -1274,7 +601,6 @@ class Class_Catalogue extends Storm_Model_Abstract {
 
     return  ($this->hasUser() &&
              ($this->getUser()->getId() == $user->getId()));
-
   }
 
 
@@ -1284,9 +610,16 @@ class Class_Catalogue extends Storm_Model_Abstract {
   }
 
 
+  public function getCreatorLoginOrFullName() {
+    return $this->hasUser()
+      ? $this->getUser()->getLoginOrFullName()
+      : '';
+  }
+
+
   public function copy() {
     $attributes = $this->getRawAttributes();
-    $attributes['libelle'] = '** Nouveau Domaine **';
+    $attributes['libelle'] = '** ' . $this->_('Nouveau Domaine') . ' **';
     unset($attributes['id']);
     unset($attributes['id_catalogue']);
     unset($attributes['ID_CATALOGUE']);
@@ -1295,68 +628,6 @@ class Class_Catalogue extends Storm_Model_Abstract {
   }
 
 
-  public function getDomainesJson($options = []) {
-    return $this->_jsonWith(
-                            'domaines_paniers',
-                            function($domain) use ($options) {
-                              return $domain->toDataForJson($options);
-                            });
-  }
-
-
-  protected function _jsonWith($id, $closure) {
-    $domaines = Class_Catalogue::getLoader()->findTopCatalogues();
-    $data_domaines=[];
-    foreach($domaines as $domaine)
-      $data_domaines [] = $closure($domaine);
-
-    return $data = ['id' => $id,
-                    'label' => $this->_('Domaines'),
-                    'categories' => $data_domaines,
-                    'items' => [],
-                    'options' => ['ico' => URL_ADMIN_IMG.'picto/domaines_16.png',
-                                  'multipleSelection' => false]];
-  }
-
-
-  public function getDomainesJsonWithoutPaniers($options = []) {
-    return $this->_jsonWith(
-                            'domaines',
-                            function($domain) use ($options) {
-                              return $domain->toDataForJsonWithoutPaniers($options);
-                            });
-
-  }
-
-
-  public function getBrowsableDomainsJson($domains = [], $options = []) {
-    $domains = $domains ? $domains : Class_Catalogue::getLoader()->findTopCatalogues();
-    $browsable_domains = array_filter($domains, function($domain) {return $domain->hasSousDomaines();});
-
-    $data_domaines=[];
-    foreach($browsable_domains as $domaine) {
-      $data_domaines [] = $domaine->getBrowsableDomainsJson($domaine->getSousDomaines(), $options);
-    }
-
-    $leaf_domains = array_diff($domains, $browsable_domains);
-    $data_leaf_domains = [];
-    foreach($leaf_domains as $domain) {
-      $data_leaf_domains [] = ['id' => $domain->getId(),
-                               'label' => $domain->getLibelle(),
-                               'options' => ['ico' => URL_ADMIN_IMG.'picto/domaines_16.png',
-                                             'multipleSelection' => false]];
-    }
-
-
-    return ['id' => $this->getId() ? $this->getId() : 'domaines',
-            'label' => $this->getLibelle() ? $this->getLibelle() : $this->_('Domaines'),
-            'categories' => $data_domaines,
-            'items' => $data_leaf_domains,
-            'options' => ['ico' => URL_ADMIN_IMG.'picto/domaines_16.png',
-                          'multipleSelection' => false]];
-
-  }
-
 
   public function hasNoSettings() {
     if ($this->isCustomDomainForm())
@@ -1513,6 +784,30 @@ class Class_Catalogue extends Storm_Model_Abstract {
 
     return $count;
   }
+
+
+  public function getLibrary() {
+    if ($parent = $this->getParent())
+      return $parent->getLibrary();
+
+    return ($library = Class_Bib::find($this->getLibraryId()))
+      ? $library
+      : Class_Bib::getPortail();
+  }
+
+
+  public function getLibraryLabel() {
+    return ($library = $this->getLibrary())
+      ? $library->getLibelle()
+      : '';
+  }
+
+
+  public function getOwnOrParentLibraryId() {
+    return ($library = $this->getLibrary())
+      ? $library->getId()
+      : null;
+  }
 }
 
 
@@ -1522,4 +817,4 @@ class AllNoticesCatalogue extends Class_Catalogue {
   public function isMatchingAllNotices() {
     return true;
   }
-}
\ No newline at end of file
+}
diff --git a/library/Class/Catalogue/Loader.php b/library/Class/Catalogue/Loader.php
new file mode 100644
index 0000000000000000000000000000000000000000..9279c91613b8b4543e4b1cb16033dfde2e885de5
--- /dev/null
+++ b/library/Class/Catalogue/Loader.php
@@ -0,0 +1,711 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+class Class_Catalogue_Loader extends Storm_Model_Loader {
+  use Trait_Translator, Trait_TimeSource;
+
+  const DEFAULT_ITEMS_BY_PAGE = 100;
+
+  protected $_indexable_cache;
+
+  public function loadNoticesFor($catalogue, $itemsByPage = self::DEFAULT_ITEMS_BY_PAGE, $page = 1, $find_all_params = null) {
+    if (null == $catalogue)
+      return [];
+
+    if ('' == ($where = $this->clausesFor($catalogue)))
+      return [];
+
+    if (!is_array($find_all_params))
+      $find_all_params = [];
+
+    if (!isset($find_all_params['limitPage']))
+      $find_all_params['limitPage'] = [$page, $itemsByPage];
+
+    $find_all_params['where'] = $where;
+
+    return Class_Notice::findAllBy($find_all_params);
+  }
+
+
+  public function findAllCataloguesAIndexer() {
+    return array_filter(Class_Catalogue::findAllIndexable(),
+                        function ($catalogue)
+                        {
+                          return '' != Class_Catalogue::clausesFor($catalogue);
+                        });
+  }
+
+
+  public function findAllIndexableNotEmpty() {
+    return array_filter(Class_Catalogue::findAllIndexable(),
+                        function($catalog)
+                        {
+                          return !$catalog->isEmpty();
+                        });
+  }
+
+
+  public function findAllIndexable() {
+    return $this->_indexable_cache
+      ? $this->_indexable_cache
+      : $this->_indexable_cache = Class_Catalogue::findAllBy(['indexer' => true,
+                                                              'order' => 'libelle']);
+  }
+
+
+  public function getDomainsForBreadcrumb($breadcrumb) {
+    $domains_ids = array_filter(explode(';', $breadcrumb));
+    $domains = [];
+    foreach ($domains_ids as $domain_id)
+      $domains[] = Class_Catalogue::find($domain_id);
+
+    return $domains;
+  }
+
+
+  public function getMultiOptionsFacets($ids) {
+    $domain_labels = ['' => $this->_('tous')];
+    if (!$domains = Class_Catalogue::findAllBy(['id_catalogue' => $ids,
+                                                'order' => 'libelle']))
+      return $domain_labels;
+
+    foreach($domains as $domain)
+      if ($th = Class_CodifThesaurus::findThesaurusForCatalogue($domain->getId()))
+        $domain_labels[$th->getFacetteIndex()]= $th->getLibelleFacette();
+
+    return $domain_labels;
+  }
+
+
+  public function updateAllThesaurusForCatalogueChildren($catalogue_id, $thesaurus_id) {
+    $catalogues = Class_Catalogue::findAllBy(['parent_id' => $catalogue_id]);
+
+    foreach ($catalogues as $catalogue) {
+      Class_CodifThesaurus::deleteAllWithIdOrigineAndCode($catalogue->getId(),
+                                                          Class_CodifThesaurus::fixedCodeOf('Domain'));
+      $new_thesaurus = $catalogue->getNewThesaurus();
+      $new_thesaurus_id = Class_CodifThesaurus::findNextThesaurusChildId('catalogue',
+                                                                         $thesaurus_id);
+      $new_thesaurus->setIdThesaurus($new_thesaurus_id);
+      $new_thesaurus->save();
+
+      Class_Catalogue::getLoader()
+        ->updateAllThesaurusForCatalogueChildren($catalogue->getId(), $new_thesaurus_id);
+    }
+  }
+
+
+  public function countNoticesFor($catalogue) {
+    if (!$catalogue)
+      return 0;
+
+    if ('' == ($where = $this->clausesFor($catalogue)))
+      return 0;
+
+    return Class_Notice::countBy(['where' => $where]);
+  }
+
+
+  public function clausesFor($catalogue) {
+    if (!$catalogue)
+      return '';
+
+    $fromUntil = $this->fromUntilClauseFor($catalogue);
+
+    if ($catalogue->isMatchingAllNotices())
+      return $fromUntil ? $fromUntil : '1=1';
+
+    $conditions = array_filter(array_merge($this->_catalogueConditions($catalogue),
+                                           [$fromUntil]));
+
+    return 0 < count($conditions)
+      ? implode(' and ', $conditions)
+      : '';
+  }
+
+
+  protected function _localClausesFor($catalogue, $against='') {
+    $conditions = [];
+
+    if ($facets = $this->facetsClauseFor($catalogue, $against))
+      $conditions[] = $facets;
+
+    if ($docType = $this->docTypeClauseFor($catalogue))
+      $conditions[] = $docType;
+
+    if ($year = $this->yearClauseFor($catalogue))
+      $conditions[] = $year;
+
+    if ($cote = $this->coteClauseFor($catalogue))
+      $conditions[] = $cote;
+
+    if ($new = $this->nouveauteClauseFor($catalogue))
+      $conditions[] = $new;
+
+    return $conditions;
+  }
+
+
+  protected function _noticesLinked($catalogue) {
+    if (!$catalogue || $catalogue->isCustomDomainForm())
+      return '';
+
+    if (!Class_NoticeDomain::getClesNoticesForDomain($catalogue->getId()))
+      return '';
+
+    return (new Class_MoteurRecherche())->prepareFacetteDomainForOrConditions($catalogue->getId());
+  }
+
+
+  /**
+   * @param $catalogue Class_Catalogue
+   * @return string
+   */
+  public function facetsClauseFor($catalogue, $against = '') {
+    $against_ou = '';
+    $facets = ['B' => $catalogue->getBibliotheque(),
+               'S' => $catalogue->getSection(),
+               'G' => $catalogue->getGenre(),
+               'L' => $catalogue->getLangue(),
+               'Y' => $catalogue->getAnnexe(),
+               'E' => $catalogue->getEmplacement(),
+               'H' => $catalogue->getThesaurusNovelty()];
+
+    foreach ($facets as $k => $v)
+      $against .= Class_Catalogue::getSelectionFacette($k, $v);
+
+    $facets = ['A' => $catalogue->getAuteur(),
+               'M' => $catalogue->getMatiere(),
+               'D' => $catalogue->getDewey(),
+               'P' => $catalogue->getPcdm4(),
+               'H' => $catalogue->getThesaurus(),
+               'Z' => $catalogue->getTags(),
+               'F' => $catalogue->getInteret()];
+
+    foreach ($facets as $k => $v)
+      $against_ou .= Class_Catalogue::getSelectionFacette($k,
+                                                          $v,
+                                                          in_array($k,
+                                                                   ['M', 'D', 'P','H']),
+                                                          false);
+
+    if ('' != $against_ou)
+      $against .= ' +(' . $against_ou . ")";
+
+    if ('' == $against)
+      return '';
+
+    return "MATCH(facettes) AGAINST('".$against."' IN BOOLEAN MODE)";
+  }
+
+
+  public function docTypeClauseFor($catalogue) {
+    if (!$docType = $catalogue->getTypeDoc())
+      return '';
+
+    $parts = array_filter(explode(';', $docType));
+    return (1 == count($parts)) ?
+      ('notices.type_doc=\'' . $parts[0]) . '\'' :
+      ('notices.type_doc IN (\'' . implode('\', \'', $parts) .  '\')');
+  }
+
+
+  public function yearClauseFor($catalogue) {
+    $clauses = [];
+    if ($start = $catalogue->getAnneeDebut())
+      $clauses[] = "annee >= '" . $start . "'";
+
+    if($end = $catalogue->getAnneeFin())
+      $clauses[] = "annee <= '" . $end . "'";
+
+    if (0 == count($clauses))
+      return '';
+
+    return implode(' and ', $clauses);
+  }
+
+
+  public function coteClauseFor($catalogue) {
+    $clauses = [];
+    if ($start = $catalogue->getCoteDebut())
+      $clauses[] = "cote >= '" . strtoupper($start) . "'";
+
+    if ($end = $catalogue->getCoteFin())
+      $clauses[] = "cote <= '". strtoupper($end) . "'";
+
+    if (0 == count($clauses))
+      return '';
+
+    return implode(' and ', $clauses);
+  }
+
+
+  public function nouveauteClauseFor($catalogue) {
+    /* Attention : la date de creation corresponds à la date de fin de nouveauté */
+    if (1 != $catalogue->getNouveaute())
+      return '';
+
+    return 'date_creation >= \'' . $this->getCurrentDate() . '\'';
+  }
+
+
+  public function fromUntilClauseFor($catalogue) {
+    $clauses = [];
+    if ($start = $catalogue->getFrom())
+      $clauses[] = "created_at >= '" . $start . "'";
+
+    if($end = $catalogue->getUntil())
+      $clauses[] = "created_at <= '" . $end . "'";
+
+    if (0 == count($clauses))
+      return '';
+
+    $clauses[] = 'created_at is not null';
+
+    return implode(' and ', $clauses);
+  }
+
+
+  /**
+   * @return array  [id => label]
+   */
+  public function allByIdLabel() {
+    return $this->byIdLabel(Class_Catalogue::findAllBy(['order' => 'libelle']));
+  }
+
+
+  /**
+   * @return array  [id => label]
+   */
+  public function byIdLabel($domains) {
+    $datas = [];
+    foreach($domains as $domain)
+      $datas[$domain->getId()] = $domain->getLibelle();
+    return $datas;
+  }
+
+
+  /**
+   * @return array
+   */
+  public function findTopCatalogues() {
+    return Class_Catalogue::findAllBy(['where' => 'parent_id is null',
+                                       'order' => 'libelle']);
+  }
+
+
+  public function findTopCataloguesPerLibraryLabel() {
+    $domains = $this->findTopCatalogues();
+    $libraries = [];
+    foreach($domains as $domain) {
+      if (!isset($libraries[$domain->getLibraryLabel()]))
+        $libraries[$domain->getLibraryLabel()] = [];
+      $libraries[$domain->getLibraryLabel()] []= $domain;
+    }
+
+    ksort($libraries, SORT_STRING);
+    return $libraries;
+  }
+
+
+  /**
+   * @return Class_Catalogue
+   */
+  public function getRoot() {
+    return Class_Catalogue::newInstanceWithId(null,
+                                              ['sous_domaines' => Class_Catalogue::findTopCatalogues()]);
+  }
+
+
+  /**
+   * @param $treeNode Trait_TreeNode
+   * @return Class_Catalogue
+   */
+  public function findWithSamePathAs($treeNode) {
+    return Class_Catalogue::getRoot()->findByPath($treeNode->getPath());
+  }
+
+
+  public function fetchAllNoticesByPreferences($preferences) {
+    $requetes = $this->getRequetes($preferences);
+    if (!array_key_exists("req_liste", $requetes))
+      return [];
+
+    $nb_par_page = (array_key_exists('aleatoire', $preferences)
+                    && array_key_exists('nb_analyse', $preferences)
+                    && $preferences['aleatoire']
+                    && $preferences['nb_analyse'])
+      ? $preferences['nb_analyse']
+      : $preferences['nb_notices'];
+
+    return Class_Notice::findAllByRequeteRecherche($requetes['req_ids'],
+                                                   $nb_par_page,
+                                                   1);
+  }
+
+
+  public function getRequetes($preferences, $fields = null) {
+    if (isset($preferences['id_panier'])
+        && (0 !== (int)$preferences['id_panier']))
+      return $this->getRequetesPanier($preferences);
+
+    $catalogue = isset($preferences['id_catalogue'])
+      ? Class_Catalogue::find($preferences['id_catalogue'])
+      : null;
+
+    if ($catalogue && $catalogue->isEmpty())
+      return [];
+
+    $against = $this->selectionFacettesForCatalogueRequestByPreferences($preferences);
+
+    $conditions = $this->_catalogueConditions($catalogue, $against);
+
+    if (isset($preferences['only_img']) && ($preferences['only_img'] == Class_Catalogue::ONLY_IMG)
+        && (($catalogue && Class_Catalogue::hasFilters($preferences['id_catalogue']))
+            || !$catalogue))
+      $conditions[] = "url_vignette > '' and url_vignette != '" . Class_WebService_Vignette::NO_DATA . "' ";
+
+    if (isset($preferences['only_img']) && ($preferences['only_img'] == Class_Catalogue::WITHOUT_IMG))
+      $conditions[] = "url_vignette=''";
+
+    $join = (isset($preferences['avec_avis']) && ($preferences['avec_avis'] == 1))
+      ? ' INNER JOIN notices_avis ON notices.clef_oeuvre=notices_avis.clef_oeuvre '
+      : '';
+
+    $order_by = $this->orderByForCatalogueRequestByPreferences($preferences);
+    $limite = $this->limitForCatalogueRequestByPreferences($preferences);
+
+    $sql = 'select %s from notices' .
+      $join .
+      (new Class_MoteurRecherche)->getConditionsForRequest(array_filter($conditions),
+                                                           $this->_noticesLinked($catalogue));
+
+    return ['req_liste' => sprintf($sql, $fields ? implode(',', $fields) : '*')
+            . $order_by . $limite,
+            'req_comptage' => sprintf($sql, 'count(*)'),
+            'req_facettes' => sprintf($sql, 'notices.id_notice, notices.type_doc, facettes')
+            . $limite,
+            'req_ids' => sprintf($sql, 'notices.id_notice') . $order_by . $limite];
+  }
+
+
+  protected function _catalogueConditions($catalogue, $against='') {
+    if (!$catalogue)
+      return $against
+        ? ["MATCH(facettes) AGAINST('" . $against . "' IN BOOLEAN MODE)"]
+        : [];
+
+    if (!$catalogue->isCustomDomainForm())
+      return $this->_localClausesFor($catalogue, $against);
+
+    $search_engine = (new Class_MoteurRecherche)
+      ->visitSearchSettings((new Class_CriteresRecherche)
+                            ->clearProfil()
+                            ->setParam('id_catalogue', $catalogue->getId()));
+
+    return !is_array($search_engine->buildWherePartQuery())
+      ? $search_engine->getConditions()
+      : [];
+  }
+
+
+  public function selectionFacettesForCatalogueRequestByPreferences($preferences) {
+    if (!isset($preferences['facettes']))
+      return '';
+
+    $against = '';
+    $facettes = explode(';', $preferences['facettes']);
+    foreach($facettes as $facette) {
+      $facette = trim($facette);
+      $against .= $this->getSelectionFacette(substr($facette, 0, 1), substr($facette, 1));
+    }
+
+    return $against;
+  }
+
+
+  public function orderByForCatalogueRequestByPreferences($preferences) {
+    if( ! array_key_exists('tri', $preferences))
+      return ' order by alpha_titre ';
+
+    $tri = $preferences['tri'];
+
+    if ($tri == '0' || $tri == 'alpha_titre')
+      return ' order by alpha_titre ';
+
+    if ( $tri == '1' || $tri == 'date_creation desc')
+      return ' order by date_creation DESC ';
+
+    if ($tri == '2' || $tri == 'nb_visu desc')
+      return ' order by nb_visu DESC ';
+
+    if ($tri == '3' || $tri == 'annee desc')
+      return ' order by annee DESC ';
+
+    return  $tri instanceof Class_MoteurRecherche_OrderCriteria
+      ? sprintf(' order by %s ', $tri->getOrder())
+      : '';
+  }
+
+
+  public function limitForCatalogueRequestByPreferences($preferences, $max_limited = false) {
+    if ( $max_limited )
+      return ' LIMIT 5000';
+
+    if ( isset($preferences["aleatoire"])
+        && 1 == (int) $preferences["aleatoire"])
+      return sprintf(' LIMIT 0,%d', (int) $preferences["nb_analyse"]);
+
+    if (isset($preferences['nb_notices']) && $preferences["nb_notices"])
+      return sprintf(' LIMIT 0,%d', (int) $preferences["nb_notices"]);
+
+    return ' LIMIT 5000'; //LL: j'ai rajouté une limite max car explosion mémoire sur des catalogues mal définis
+  }
+
+
+  public function getRequetesPanier($preferences) {
+    $panier = null;
+    if (array_key_exists('id_user', $preferences))
+      $panier = Class_PanierNotice::findFirstBy(['id_user' => $preferences['id_user'],
+                                                 'id' => $preferences['id_panier']]);
+    if (!$panier)
+      $panier = Class_PanierNotice::find($preferences['id_panier']);
+
+    if (!$panier)
+      return ['nombre' => 0];
+
+    $cles_notices = $panier->getClesNotices();
+    if (empty($cles_notices))
+      return ['nombre' => 0];
+
+    $keys = [];
+    foreach($cles_notices as $notice) {
+      if (!trim($notice))
+        continue;
+      $keys[] = "'" . $notice . "'";
+    }
+    $in_sql = implode(',', $keys);
+
+    $limite = ($preferences['aleatoire'] == 1)
+      ? $preferences['nb_analyse'] : $preferences['nb_notices'];
+    $limite = ($limite) ? 'LIMIT 0,' . $limite : '';
+
+    $order_by = '';
+
+    if (!isset($preferences["tri"]))
+      $preferences["tri"] = 0;
+
+    if ($preferences["tri"]==0)
+      $order_by=" order by alpha_titre ";
+    if ($preferences["tri"]==1)
+      $order_by=" order by date_creation DESC ";
+    if ($preferences["tri"]==2)
+      $order_by=" order by nb_visu DESC ";
+    if($preferences['tri'] > 2)
+      $order_by = ' order by FIELD(notices.clef_alpha, ' . $in_sql  . ') ';
+
+    $condition = (array_isset("only_img", $preferences)
+                  && $preferences["only_img"] == 1)
+      ? " and url_vignette > '' and url_vignette != '" . Class_WebService_Vignette::NO_DATA . "'"
+      : '';
+    $condition .= ' and type=1 ';
+
+    $join = (array_isset("avec_avis", $preferences) && $preferences["avec_avis"] == 1)
+      ? ' INNER JOIN notices_avis ON notices.clef_oeuvre=notices_avis.clef_oeuvre '
+      : '';
+
+    $sql = 'select %s from notices '
+      . $join
+      . 'where notices.clef_alpha in(' . $in_sql . ')'
+      . $condition;
+
+    return ['req_liste' => sprintf($sql, '*') . $order_by . $limite,
+            'req_comptage' => sprintf($sql, 'count(*)'),
+            'req_facettes' => sprintf($sql, 'id_notice, notices.type_doc, facettes') . $limite,
+            'req_ids' => sprintf($sql, 'notices.id_notice') . $order_by . $limite];
+  }
+
+
+  public function getNoticesFromCacheByPreferences($preferences) {
+    $callback = function() use ($preferences) {
+      return Class_Catalogue::getLoader()->fetchAllNoticesByPreferences($preferences);
+    };
+
+    return (array_key_exists('aleatoire', $preferences)
+            && $preferences['aleatoire'])
+      ? $callback()
+      : (new Storm_Cache())->memoize([$preferences, __CLASS__, __FUNCTION__], $callback);
+  }
+
+
+  public function getNoticesByPreferences($preferences) {
+    if (isset($preferences['id_catalogue'])
+        && ($catalogue = Class_Catalogue::getLoader()->find($preferences['id_catalogue']))) {
+      $preferences['catalogue_cache_key'] = serialize($catalogue->toArray());
+    }
+
+    $notices = $this->getNoticesFromCacheByPreferences($preferences);
+
+    if ((int)$preferences["aleatoire"] !== 1)
+      return $notices;
+
+    shuffle($notices);
+    return array_slice ($notices, 0, $preferences["nb_notices"]);
+  }
+
+
+  public function hasFilters($id) {
+    if(!$catalogue = Class_Catalogue::find($id))
+      return false;
+
+    return !$catalogue->hasNoSettings();
+  }
+
+
+  public function hasFiltersOrLinkedRecords($id) {
+    return ($catalog = Class_Catalogue::find((int)$id))
+      ? !$catalog->isEmpty()
+      : false;
+  }
+
+
+  public function saveThesaurus($catalogue) {
+    if ($thesaurus = Class_CodifThesaurus::findThesaurusForCatalogue($catalogue->getId())) {
+      $catalogue->deleteThesaurusInFacette($thesaurus->getIdThesaurus());
+      $catalogue->updateThesaurusLabel($thesaurus);
+    } else if (!$thesaurus = $catalogue->saveThesauriParents())
+      return;
+
+    if($catalogue->hasDomaineParent()){
+      $parent=$catalogue->getParent();
+      $thesaurus_parent = $parent->saveThesauriParents();
+      if (!$thesaurus_parent)
+        return null;
+      $new_thesaurus_id=Class_CodifThesaurus::findNextThesaurusChildId(
+                                                                       'catalogue',
+                                                                       $thesaurus_parent->getIdThesaurus());
+    }
+    else if (strlen($thesaurus->getIdThesaurus())>8) {
+      $new_thesaurus_id=Class_CodifThesaurus::findNextRacineCatalogue();}
+    else
+      return $thesaurus;
+
+    if ($thesaurus->getId() && ($new_thesaurus_id != $thesaurus->getIdThesaurus())) {
+      $thesaurus->setIdThesaurus($new_thesaurus_id);
+      $thesaurus->setLibelle($catalogue->getLibelle());
+      $thesaurus->save();
+
+      Class_Catalogue::updateAllThesaurusForCatalogueChildren($catalogue->getId(),$new_thesaurus_id);
+      return $thesaurus;
+    }
+
+    $thesaurus->setIdThesaurus($new_thesaurus_id);
+    $thesaurus->setLibelle($catalogue->getLibelle());
+    $thesaurus->save();
+
+    return $thesaurus;
+  }
+
+
+  public function getIds($domains) {
+    return (new Storm_Model_Collection($domains))
+      ->collect('id')
+      ->getArrayCopy();
+  }
+
+
+  public function hasViewableDomain() {
+    return 0 < count(Class_Catalogue::findAllIndexableNotEmpty());
+  }
+
+
+  public function getSelectionFacette($type, $valeurs, $descendants = false, $signe = true) {
+    if ( !$valeurs = array_filter(explode(';', $valeurs)))
+      return false;
+
+    $cond = '';
+    foreach ($valeurs as $valeur) {
+      if (!$valeur)
+        continue;
+
+      if (!$descendants) {
+        $cond .= $type . $valeur . ' ';
+        continue;
+      }
+
+      if ('M' != $type) {
+        $cond .= $type . $valeur . '* ';
+        continue;
+      }
+
+      if (!$matiere = Class_Matiere::find($valeur))
+        continue;
+
+      if ('' != ($sous_vedettes = trim($matiere->getSousVedettes())))
+        $valeur .= str_replace(' ', ' M', ' ' . $sous_vedettes);
+      $cond .= $type . $valeur . ' ';
+    }
+
+    $cond = trim($cond);
+
+    return ($signe) ? ' +(' . $cond . ')' : ' ' . $cond;
+  }
+
+
+  public function newCatalogueForAll() {
+    return new AllNoticesCatalogue();
+  }
+
+
+  public function getCataloguesForCombo()  {
+    if (!$catalogues = Class_Catalogue::findTopCatalogues())
+      return [];
+
+    $liste = [''];
+    foreach($catalogues as $catalogue) {
+      $this->addCataloguePathAndChildrenTo($catalogue, $liste);
+    }
+    return $liste;
+  }
+
+
+  public function addCataloguePathAndChildrenTo($catalogue, &$liste) {
+    $liste[$catalogue->getId()] = implode(' > ', $catalogue->getPathParts());
+    $sous_domaines = $catalogue->getSousDomaines();
+    foreach($sous_domaines as $sous_domaine)
+      $this->addCataloguePathAndChildrenTo($sous_domaine, $liste);
+  }
+
+
+  public function findParentChildrenOf($model) {
+    if ($model->isNew())
+      return array_filter(Class_Catalogue::findTopCatalogues(),
+                          [$this, '_isParent']);
+
+    $children = $model->getSousDomaines();
+    return array_filter($children, [$this, '_isParent']);
+  }
+
+
+  protected function _isParent($model) {
+    return $model->hasSousDomaines();
+  }
+}
diff --git a/library/Class/Catalogue/TreeSelectJson.php b/library/Class/Catalogue/TreeSelectJson.php
new file mode 100644
index 0000000000000000000000000000000000000000..70f0453af582886c98a611c6cb0246d6b6441be5
--- /dev/null
+++ b/library/Class/Catalogue/TreeSelectJson.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Catalogue_TreeSelectJson {
+  use Trait_Translator;
+
+  public function render($options = []) {
+    return $this->_jsonWith(
+                            'domaines_paniers',
+                            function($domain) use ($options) {
+                              return $domain->toDataForJson($options);
+                            });
+  }
+
+
+  public function renderWithoutCheckbox() {
+    return $this->render(['removeCheckbox' => true]);
+  }
+
+
+  public function renderWithoutCarts() {
+    return [$this->_jsonWith(
+                             'domaines',
+                             function($domain) {
+                               return $domain->toDataForJsonWithoutPaniers();
+                             })];
+  }
+
+
+  protected function _jsonWith($id, $closure) {
+    $root = ['id' => $id,
+             'label' => $this->_('Domaines'),
+             'categories' => [],
+             'items' => [],
+             'options' => ['ico' => URL_ADMIN_IMG.'picto/domaines_16.png',
+                           'multipleSelection' => false]];
+
+    if (!Class_AdminVar::isModuleEnabled('ENABLE_DOMAINS_PER_LIBRARIES')) {
+      $root['categories'] = array_map($closure, Class_Catalogue::findTopCatalogues());
+      return $root;
+    }
+
+
+    foreach(Class_Catalogue::findTopCataloguesPerLibraryLabel() as $library_label => $domains) {
+      $root['categories'][]= ['id' => $library_label,
+                              'label' => $library_label,
+                              'categories' => array_map($closure, $domains),
+                              'items' => [],
+                              'options' => ['ico' => URL_ADMIN_IMG.'picto/bibliotheques_16.png',
+                                            'removeCheckbox' => true]];
+    }
+
+    return  $root;
+  }
+
+
+  public function renderBrowsableDomains() {
+    $top_domains = Class_AdminVar::isModuleEnabled('ENABLE_DOMAINS_PER_LIBRARIES')
+      ? Class_Catalogue::findTopCataloguesPerLibraryLabel()
+      : [$this->_('Domaines') => Class_Catalogue::findTopCatalogues()];
+
+    $datas = [];
+
+    foreach($top_domains as $library_label => $domains) {
+      $datas[] = ['id' => $library_label,
+                  'label' => $library_label,
+                  'categories' => array_map(
+                                            function($domain) {
+                                              return $this->getBrowsableDomainsJson($domain);
+                                            },
+                                            $domains),
+                  'items' => [],
+                  'options' => []];
+    }
+
+    return $datas;
+  }
+
+
+  public function getBrowsableDomainsJson($parent, $options = []) {
+    $domains = $parent->getSousDomaines();
+    $browsable_domains = array_filter($domains,
+                                      function($domain) {
+                                        return $domain->hasSousDomaines();
+                                      });
+
+    $data_domaines=[];
+    foreach($browsable_domains as $parent_domain) {
+      $data_domaines [] = $this->getBrowsableDomainsJson($parent_domain,
+                                                         $options);
+    }
+
+    $leaf_domains = array_diff($domains, $browsable_domains);
+    $data_leaf_domains = [];
+    foreach($leaf_domains as $domain) {
+      $data_leaf_domains [] = ['id' => $domain->getId(),
+                               'label' => $domain->getLibelle(),
+                               'options' => ['ico' => URL_ADMIN_IMG.'picto/domaines_16.png',
+                                             'multipleSelection' => false]];
+    }
+
+
+    return ['id' => $parent->getId(),
+            'label' => $parent->getLibelle(),
+            'categories' => $data_domaines,
+            'items' => $data_leaf_domains,
+            'options' => ['ico' => URL_ADMIN_IMG.'picto/domaines_16.png',
+                          'multipleSelection' => false]];
+
+  }
+}
diff --git a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Abstract.php b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Abstract.php
index 6516269fb36e5ca26e4b18c887b332286f2599df..096b9d298a62710f04354c55436f077bb35ba347 100644
--- a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Abstract.php
+++ b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Abstract.php
@@ -55,7 +55,7 @@ abstract class ZendAfi_Controller_Action_Helper_ListViewMode_Abstract
 
 
   public function getCategoriesDescription() {
-    if ($this->isSearching())
+    if ($this->isSearching() && !$this->isSearchDisplayCategories())
       return;
 
     $model_class = $this->getModel() ? get_class($this->getModel()) : '';
@@ -82,7 +82,9 @@ abstract class ZendAfi_Controller_Action_Helper_ListViewMode_Abstract
     if ($this->isCountEnabled())
       $label .= ' (' . ($count = $this->countItemsInTreeFrom($model)) . ')';
 
-    return $this->_view->tagAnchor($url, $label, ['data-count' => $count]);
+    $html = $this->_view->tagAnchor($url, $label, ['data-count' => $count]);
+    return $this->_decorateModelWithSearchBreadcrumb($html,
+                                                     $model);
   }
 
 
@@ -108,13 +110,20 @@ abstract class ZendAfi_Controller_Action_Helper_ListViewMode_Abstract
 
   protected function _renderItem($model, $attrib) {
     $value = $model->callGetterByAttributeName($attrib);
+    return $this->_decorateModelWithSearchBreadcrumb($value, $model);
+  }
+
+
+  protected function _decorateModelWithSearchBreadcrumb($value, $model) {
     if (!$this->isSearching())
       return $value;
 
-    return $this->_view->tag('span', $value)
-      . $this->_view->tag('p',
-                          $this->_getBreadcrumbHtmlFor($this->getBreadcrumbFor($this->getCategoryFor($model))),
-                          ['style' => 'font-size:0.9em;']);
+    return
+      $this->_view->tag('span', $value)
+      .
+      $this->_view->tag('p',
+                        $this->_getBreadcrumbHtmlFor($this->getBreadcrumbFor($this->getCategoryFor($model))),
+                        ['style' => 'font-size:0.9em;']);
   }
 
 
@@ -141,6 +150,11 @@ abstract class ZendAfi_Controller_Action_Helper_ListViewMode_Abstract
   }
 
 
+  public function isSearchDisplayCategories() {
+    return false;
+  }
+
+
   protected function enabledPager() {
     return false;
   }
@@ -284,7 +298,7 @@ abstract class ZendAfi_Controller_Action_Helper_ListViewMode_Abstract
    * @return array
    */
   protected function getBreadcrumbFor($model, $breadcrumb=[], $start_key='') {
-    if (!$model)
+    if (!$model || $model->isNew())
       return $breadcrumb;
 
     if (!$this->_shouldCheckParent($start_key, $model)
@@ -471,4 +485,4 @@ abstract class ZendAfi_Controller_Action_Helper_ListViewMode_Abstract
   protected function _sqlQuote($value) {
     return Zend_Registry::get('sql')->quote($value);
   }
-}
\ No newline at end of file
+}
diff --git a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Catalogue.php b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Catalogue.php
new file mode 100644
index 0000000000000000000000000000000000000000..4cdf761a21bb586c7f09e6d5f6a41d184801ffca
--- /dev/null
+++ b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Catalogue.php
@@ -0,0 +1,199 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class ZendAfi_Controller_Action_Helper_ListViewMode_Catalogue
+  extends ZendAfi_Controller_Action_Helper_ListViewMode_Abstract {
+
+
+  public function init() {
+    parent::init();
+    $this->_form_settings->setPlaceHolder($this->_('libellé du domaine'));
+  }
+
+
+  public function ListViewMode_Catalogue($params) {
+    $this->_params = $params;
+    return $this;
+  }
+
+
+  public function direct($params) {
+    return $this->ListViewMode_Catalogue($params);
+  }
+
+
+  public function isSearchDisplayCategories() {
+    return true;
+  }
+
+
+  protected function _describeDomainList($description, $title, $render_callback) {
+    $description
+      ->setSorterServer()
+      ->addColumn($title,
+                  ['attribute' => 'libelle',
+                   'sortable' => false,
+                   'callback' => $render_callback])
+      ->addColumn($this->_('Créateur'), ['attribute' => 'creator_login_or_full_name',
+                                         'sortable' => false])
+      ->addColumn($this->_('Description'), ['attribute' => 'description',
+                                            'sortable' => false]);
+
+    return $description;
+  }
+
+
+  protected function _describeCategoriesIn($description) {
+    return $this->_describeDomainList($description,
+                                      $this->_('Liste des domaines avec sous-domaines'),
+                                      function ($model, $attrib)
+                                      {
+                                        return $this->_renderCategory($model, $attrib);
+                                      });
+  }
+
+
+  protected function countItemsFor($domain) {
+    return $domain ? $domain->numberOfSousDomaines() : 0;
+  }
+
+
+
+  protected function _describeItemsIn($description) {
+    return $this->_describeDomainList($description,
+                                      $this->_('Liste des domaines'),
+                                      function ($model, $attrib)
+                                      {
+                                        return $this->_renderItem($model, $attrib);
+                                      });
+  }
+
+
+  protected function _allDomains() {
+    return $this->isSearching()
+      ? $this->_searchDomains()
+      : $this->_browseDomains();
+
+    return $domains;
+  }
+
+
+  protected function _searchDomains() {
+    $params = ['order' => 'libelle',
+               'libelle like' => '%' . $this->getSearchValue() . '%'];
+
+    $domains = new Storm_Model_Collection(Class_Catalogue::findAllBy($params));
+    $library_id = $this->getParam('id_bib', null);
+
+   return $domains->select(
+                           function($domain) use ($library_id)
+                           {
+                             return $domain->getOwnOrParentLibraryId() == $library_id;
+                           });
+  }
+
+
+  protected function _browseDomains() {
+    $parent_id = $this->getModel()->getId();
+    $params = ['order' => 'libelle',
+               'parent_id' => $parent_id];
+
+    if ((null === $parent_id) && array_key_exists('id_bib', $this->_params))
+      $params['library_id'] = $this->_params['id_bib']
+        ? $this->_params['id_bib']
+        : null;
+    return new Storm_Model_Collection(Class_Catalogue::findAllBy($params));
+  }
+
+
+  public function getItems() {
+    return $this->_allDomains()
+      ->reject('hasSousDomaines')
+      ->getArrayCopy();
+  }
+
+
+  public function getCategories() {
+    return $this->_allDomains()
+      ->select('hasSousDomaines')
+      ->getArrayCopy();
+  }
+
+
+  public function getDefaultModel() {
+    return ($this->getModel() && !$this->getModel()->isNew())
+      ? $this->getModel()
+      : $this->_getLibrary();
+  }
+
+
+
+  public function getBaseUrl() {
+    return ['module' => 'admin',
+            'controller' => 'catalogue',
+            'action' => 'index',
+            'id_bib' => (array_key_exists('id_bib', $this->_params)
+                         ? $this->_params['id_bib']
+                         : null)];
+  }
+
+
+  public function getBreadcrumb() {
+    $breadcrumb = [
+                   ['url' => ['module' => 'admin',
+                              'controller' => 'catalogue',
+                              'action' => 'index'],
+                    'label' => $this->_('Racine'),
+                    'options' => []
+                   ],
+
+                   ['url' => array_merge($this->getBaseUrl(),
+                                         ['id_bib' => $this->_getLibraryId()]),
+                    'label' => $this->_getLibraryLabel(),
+                    'options' => []]
+    ];
+
+    return array_merge($breadcrumb,
+                       $this->getBreadcrumbFor($this->getModel()));
+  }
+
+
+  protected function _getLibrary() {
+    return $this->getParam('bib', Class_Bib::getPortail());
+  }
+
+
+  protected function _getLibraryLabel() {
+    return $this->_getLibrary()->getLibelle();
+  }
+
+
+  protected function _getLibraryId() {
+    return $this->_getLibrary()->getId();
+  }
+
+
+  public function isSearchEnabled() {
+    return true;
+  }
+
+}
diff --git a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Library.php b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Library.php
index 373cd3f43f6de3d1a8372decb941ecf9bd0964b8..8a04d0e83ef5a99c347e602b908f84358ae6985c 100644
--- a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Library.php
+++ b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Library.php
@@ -21,7 +21,6 @@
 
 
 class ZendAfi_Controller_Action_Helper_ListViewMode_Library extends ZendAfi_Controller_Action_Helper_ListViewMode_Abstract {
-
   public function ListViewMode_Library($params) {
     return parent::_initParams($params);
   }
@@ -32,6 +31,11 @@ class ZendAfi_Controller_Action_Helper_ListViewMode_Library extends ZendAfi_Cont
   }
 
 
+  public function getItemsDescription() {
+    return null;
+  }
+
+
   protected function _describeCategoriesIn($description) {
     return $description
       ->addColumn($this->_('Localisation'), ['attribute' => 'libelle',
@@ -49,7 +53,7 @@ class ZendAfi_Controller_Action_Helper_ListViewMode_Library extends ZendAfi_Cont
 
   public function getBaseUrl() {
     return ['module' => 'admin',
-            'controller' => 'cms'];
+            'controller' => $this->getRequest()->getControllerName()];
   }
 
 
@@ -76,4 +80,4 @@ class ZendAfi_Controller_Action_Helper_ListViewMode_Library extends ZendAfi_Cont
   public function getStrategyLabel() {
     return 'bib';
   }
-}
\ No newline at end of file
+}
diff --git a/library/ZendAfi/Controller/Plugin/Manager/Catalogue.php b/library/ZendAfi/Controller/Plugin/Manager/Catalogue.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb5fd8aa23fc924697b26f4bfd6c2396e871aeb5
--- /dev/null
+++ b/library/ZendAfi/Controller/Plugin/Manager/Catalogue.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class ZendAfi_Controller_Plugin_Manager_Catalogue extends ZendAfi_Controller_Plugin_Manager_Manager {
+  public function getActions($model) {
+    if (!$model)
+      return [];
+
+    if (Class_Catalogue::class == get_class($model))
+      return $this->_getDomainActions($model);
+
+    if (Class_Bib::class == get_class($model))
+      return $this->_getLibraryActions($model);
+
+    return [];
+  }
+
+
+  protected function _getLibraryActions($library) {
+    $actions = [
+                ['url' => '/admin/catalogue/add/id_bib/' . $library->getId(),
+                 'icon' => 'add_page',
+                 'label' => $this->_('Ajouter un domaine à la bibliothèque %s',
+                                     $library->getLibelle())]
+    ];
+
+    return $actions;
+  }
+
+
+  protected function _getDomainActions($domain) {
+    $editable = $domain->canBeDeletedOrModifyByUser(Class_Users::getIdentity());
+    $actions = [['url' => '/admin/catalogue/tester/id_catalogue/%s',
+                 'icon' => 'test',
+                 'label' => $this->_('Indexer les notices du domaine %s',
+                                     $domain->getLibelle())]];
+
+    if ($editable)
+      $actions []= ['url' => '/admin/catalogue/edit/id_catalogue/%s',
+                    'icon' => 'edit',
+                    'label' => $this->_('Modifier le domaine %s',
+                                        $domain->getLibelle())];
+
+    $actions []= ['url' => '/admin/catalogue/duplicate/id_catalogue/%s',
+                  'icon' => 'copy',
+                  'label' => $this->_('Dupliquer le domaine %s',
+                                      $domain->getLibelle())];
+
+    if ($editable) {
+      $actions []= ['url' => '/admin/catalogue/add/id_catalogue/%s',
+                    'icon' => 'add_page',
+                    'label' => $this->_('Ajouter un sous-domaine au domaine %s',
+                                        $domain->getLibelle())];
+      $actions []= ['url' => '/admin/catalogue/paniers/id_catalogue/%s',
+                    'icon' => 'basket',
+                    'label' => $this->_('Paniers du domaine %s',
+                                        $domain->getLibelle())];
+
+      $actions []= ['url' => '/admin/catalogue/delete/id_catalogue/%s',
+                    'icon' => 'delete',
+                    'label' => $this->_('Supprimer le domaine %s',
+                                        $domain->getLibelle())];
+    }
+
+    $actions []= ['url' => ['module' => 'opac',
+                            'controller' => 'recherche',
+                            'action' => 'simple',
+                            'id_catalogue' => $domain->getId()],
+                  'icon' => 'view',
+                  'label' => $this->_('Visualiser le domaine "%s" dans un nouvel onglet',
+                                      $domain->getLibelle()),
+                  'anchorOptions' => ['target' => '_blank']];
+
+    $actions []= ['url' => null,
+                  'icon' => 'permalink',
+                  'label' => $this->_('Voir le lien permanent du domaine "%s"',
+                                      $domain->getLibelle()),
+                  'anchorOptions' => ['data-url' => '/recherche/simple/id_catalogue/' . $domain->getId(),
+                                      'data-helptext' => $this->_('Copiez le lien suivant'),
+                                      'title' => $this->_('Lien permanent'),
+                                      'onclick' => 'popupPermalink(this);return false']];
+    return $actions;
+  }
+}
diff --git a/library/ZendAfi/View/Helper/Admin/ComboCatalogue.php b/library/ZendAfi/View/Helper/Admin/ComboCatalogue.php
index 65005e9844db6b77a9a68549a9e54af183a6b215..1dc9330a96842848a9ba4395b60bab6c81305a00 100644
--- a/library/ZendAfi/View/Helper/Admin/ComboCatalogue.php
+++ b/library/ZendAfi/View/Helper/Admin/ComboCatalogue.php
@@ -34,7 +34,23 @@ class ZendAfi_View_Helper_Admin_ComboCatalogue extends ZendAfi_View_Helper_BaseH
 
 
   public function _getAllCatalogues() {
-    return $this->_getCatalogs(Class_Catalogue::findTopCatalogues());
+    return Class_AdminVar::isModuleEnabled('ENABLE_DOMAINS_PER_LIBRARIES')
+      ? $this->_getCatalogsPerLibrary(Class_Catalogue::findTopCataloguesPerLibraryLabel())
+      : $this->_getCatalogs(Class_Catalogue::findTopCatalogues());
+  }
+
+
+  protected function _getCatalogsPerLibrary($libraries) {
+    $html = '';
+    foreach($libraries as $label => $domains) {
+      $html .= ($visible_domains = $this->_getCatalogs($domains))
+        ? $this->_tag('optgroup',
+                      $visible_domains,
+                      ['label' => $label])
+        : '';
+    }
+
+    return $html;
   }
 
 
@@ -67,4 +83,4 @@ class ZendAfi_View_Helper_Admin_ComboCatalogue extends ZendAfi_View_Helper_BaseH
   }
 }
 
-?>
\ No newline at end of file
+?>
diff --git a/library/ZendAfi/View/Helper/PartialCycle.php b/library/ZendAfi/View/Helper/PartialCycle.php
index c9bb48c3a55ec2e6d14a244e21c329fa5dab1fa8..de1bacea483c4c4bf3a3be98513cb5c970b854e9 100644
--- a/library/ZendAfi/View/Helper/PartialCycle.php
+++ b/library/ZendAfi/View/Helper/PartialCycle.php
@@ -21,7 +21,7 @@
 
 class ZendAfi_View_Helper_PartialCycle extends Zend_View_Helper_Partial
 {
-  public function partialCycle($name, $key, $models, $class_cycle)  {
+  public function partialCycle($name, $key, $models, $class_cycle, $extra_variables=[])  {
     $content = '';
     end($class_cycle);
 
@@ -29,11 +29,13 @@ class ZendAfi_View_Helper_PartialCycle extends Zend_View_Helper_Partial
       if (false == $item_class = next($class_cycle))
         $item_class = reset($class_cycle);
 
-      $content .= $this->partial($name, [$key => $item,
-                                         'item_class' => $item_class]);
+      $content .= $this->partial($name,
+                                 array_merge([$key => $item,
+                                              'item_class' => $item_class],
+                                             $extra_variables));
     }
     return $content;
   }
 }
 
-?>
\ No newline at end of file
+?>
diff --git a/library/storm b/library/storm
index 3e243b7852935d6613857b2b07a083ce234077bf..27ef84c18e7820736efebc43c0817fe7d32b9a52 160000
--- a/library/storm
+++ b/library/storm
@@ -1 +1 @@
-Subproject commit 3e243b7852935d6613857b2b07a083ce234077bf
+Subproject commit 27ef84c18e7820736efebc43c0817fe7d32b9a52
diff --git a/tests/application/modules/admin/controllers/CatalogueControllerTest.php b/tests/application/modules/admin/controllers/CatalogueControllerTest.php
index 33e01eccd1707a540476cc045d07ec1e714b4b3a..5d186dd62844b833a7469f242a4cd015c7673f25 100644
--- a/tests/application/modules/admin/controllers/CatalogueControllerTest.php
+++ b/tests/application/modules/admin/controllers/CatalogueControllerTest.php
@@ -366,7 +366,7 @@ class CatalogueControllerWithModoPortailTotalAccessIndexTest extends AdminCatalo
 
   /** @test */
   public function pageShouldContainsPermalinkForCataloguePolitique() {
-    $this->assertXPath('//img[contains(@class, "permalink")][contains(@data-url, "recherche/simple/id_catalogue/200")]');
+    $this->assertXPath('//a[@title="Lien permanent"][contains(@data-url, "recherche/simple/id_catalogue/200")]');
   }
 
 
diff --git a/tests/db/UpgradeDBTest.php b/tests/db/UpgradeDBTest.php
index 47a8a956cd2eddcdf0896a6236eba5c1b2ff252a..ca6651fcbe644f4773e79f28a32c5ecd68c7b6be 100644
--- a/tests/db/UpgradeDBTest.php
+++ b/tests/db/UpgradeDBTest.php
@@ -3919,7 +3919,7 @@ class UpgradeDB_412_Test extends UpgradeDBTestCase {
 
 class UpgradeDB_413_Test extends UpgradeDBTestCase {
   public function prepare() {
-    $this->silentQuery("alter table alter drop column interet");
+    $this->silentQuery("alter table album drop column interet");
   }
 
 
@@ -3982,4 +3982,26 @@ class UpgradeDB_415_Test extends UpgradeDBTestCase {
   public function tableCmsArticleTimingShouldHaveField($field, $type) {
     $this->assertFieldType('cms_article_timings', $field, $type);
   }
-}
\ No newline at end of file
+}
+
+
+
+
+class UpgradeDB_416_Test extends UpgradeDBTestCase {
+  public function prepare() {
+    $this->silentQuery("alter table catalogue drop column library_id");
+  }
+
+
+  /** @test */
+  public function catalogueShouldHaveColumnLibraryIdInt() {
+    $this->assertFieldType('catalogue', 'library_id', 'int(11) unsigned');
+  }
+
+
+  /** @test */
+  public function catalogueShouldHaveIndexOnLibraryId() {
+    $this->assertIndex('catalogue', 'library_id');
+  }
+
+}
diff --git a/tests/library/Class/CatalogueTest.php b/tests/library/Class/CatalogueTest.php
index 7f927b4b09fac53fb04a19a76adae3b60467315c..fb320bcefbf014eb6d649efb7fc8b7d10a901c38 100644
--- a/tests/library/Class/CatalogueTest.php
+++ b/tests/library/Class/CatalogueTest.php
@@ -454,7 +454,7 @@ class CatalogueTestGetPagedNotices extends ModelTestCase {
 
   protected function _expectNoticeFindAllBy($where, $limit = []) {
     if (0 == count($limit))
-      $limit = [1, CatalogueLoader::DEFAULT_ITEMS_BY_PAGE];
+      $limit = [1, Class_Catalogue_Loader::DEFAULT_ITEMS_BY_PAGE];
 
     $this->_noticeWrapper
       ->whenCalled('findAllBy')
@@ -544,7 +544,7 @@ class CatalogueTestOAISpec extends ModelTestCase {
 
 
 class CatalogueGetNoticesByPreferencesNotRandomTest extends ModelTestCase {
-  protected $_cache_key = '5be5b882d00792a8bcf7299b575bb175';
+  protected $_cache_key = '25d43b0fa219225c79624883f5f7fd99';
 
   public function setUp() {
     parent::setUp();
diff --git a/tests/scenarios/DomainsPerLibraries/DomainsPerLibrariesTest.php b/tests/scenarios/DomainsPerLibraries/DomainsPerLibrariesTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ee7867af5157c08b624ed07e64f9c1a8a0f5a657
--- /dev/null
+++ b/tests/scenarios/DomainsPerLibraries/DomainsPerLibrariesTest.php
@@ -0,0 +1,870 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+abstract class DomainsPerLibrariesTestCase extends Admin_AbstractControllerTestCase {
+  protected
+    $_storm_default_to_volatile = true;
+
+
+  public function setUp() {
+    parent::setUp();
+    Class_AdminVar::set('ENABLE_DOMAINS_PER_LIBRARIES', 1);
+
+    $this->fixture(Class_Catalogue::class,
+                   ['id' => 1,
+                    'libelle' => 'All places',
+                    'library_id' => null,
+                    'parent_id' => null]);
+
+    $this->fixture(Class_Catalogue::class,
+                   ['id' => 2,
+                    'libelle' => 'All characters',
+                    'library_id' => null,
+                    'parent_id' => null]);
+
+    $this->fixture(Class_Catalogue::class,
+                   ['id' => 3,
+                    'libelle' => 'Wookiees',
+                    'library_id' => null,
+                    'parent_id' => 2]);
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 1,
+                    'libelle' => 'Alderaan']);
+
+    $this->fixture(Class_Users::class,
+                   ['id' => 8,
+                    'login' => 'alderaan_admin',
+                    'password' => 'secret',
+                    'bib' => Class_Bib::find(1)])
+         ->beAdminBib();
+
+    $this->fixture(Class_Catalogue::class,
+                   ['id' => 4,
+                    'libelle' => 'Alderaan places',
+                    'library_id' => 1,
+                    'id_user' => 8,
+                    'description' => 'Cool places on Alderaan',
+                    'parent_id' => null]);
+
+    $this->fixture(Class_Catalogue::class,
+                   ['id' => 5,
+                    'libelle' => 'Alderaan towns',
+                    'library_id' => null,
+                    'parent_id' => 4]);
+
+    $this->fixture(Class_Catalogue::class,
+                   ['id' => 6,
+                    'libelle' => 'Alderaan characters',
+                    'library_id' => null,
+                    'parent_id' => 4]);
+
+    $this->fixture(Class_Bib::class,
+                   ['id' => 2,
+                    'libelle' => 'Tatooine']);
+
+    $this->fixture(Class_Catalogue::class,
+                   ['id' => 7,
+                    'libelle' => 'Tatooine characters',
+                    'library_id' => 2,
+                    'parent_id' => null]);
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerIndexTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/catalogue/index');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTableWithFolderLinkToPortal() {
+    $this->assertXPathContentContains('//table//td/a[contains(@href, "/admin/catalogue/index/id_bib/0")]',
+                                      'Portail');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTableWithFolderLinkToAlderaan() {
+    $this->assertXPathContentContains('//table//td/a[contains(@href, "/admin/catalogue/index/id_bib/1")]',
+                                      'Alderaan');
+  }
+
+
+  /** @test */
+  public function breadShouldContainsPortalActionToAddNewDomain() {
+    $this->assertXPathContentContains('//table//td//a/@href',
+                                      '/admin/catalogue/add/id_bib/0');
+
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsItemsTable() {
+    $this->assertNotXPath('//table[contains(@id, "items_")]');
+  }
+}
+
+
+
+class DomainsPerLibrariesCatalogueControllerAsAdminBibWithoutGroupIndexTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    ZendAfi_Auth::getInstance()
+      ->logUser(Class_Users::findFirstBy(['login' => 'alderaan_admin']));
+
+    $this->dispatch('/admin/catalogue/index');
+
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTableWithFolderLinkToPortal() {
+    $this->assertXPathContentContains('//table//td/a[contains(@href, "/admin/catalogue/index/id_bib/0")]',
+                                      'Portail');
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerAsAdminAlderaanWithAccessIndexTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $domain_admins = $this->fixture(Class_UserGroup::class,
+                                    ['id' => 4,
+                                     'libelle' => 'Domains admins',
+                                     'rights' => [Class_UserGroup::RIGHT_USER_DOMAINES_TOTAL_ACCESS]]);
+
+    ZendAfi_Auth::getInstance()
+      ->logUser(Class_Users::findFirstBy(['login' => 'alderaan_admin'])
+                ->addUserGroup($domain_admins));
+
+    $this->dispatch('/admin/catalogue/index');
+  }
+
+
+  /** @test */
+  public function leftAdminMenuShouldHaveEntryToAdminCatalogue() {
+    $this->assertXPathContentContains('//ul[@class="menuAdmin"]//a/@href',
+                                      'admin/catalogue');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTableWithFolderLinkToPortal() {
+    $this->assertXPathContentContains('//table//td/a[contains(@href, "/admin/catalogue/index/id_bib/0")]',
+                                      'Portail');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTableWithFolderLinkToAlderaan() {
+    $this->assertXPathContentContains('//table//td/a[contains(@href, "/admin/catalogue/index/id_bib/1")]',
+                                      'Alderaan');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsTableWithFolderLinkToTatooine() {
+    $this->assertNotXPath('//table//td/a[contains(@href, "/admin/catalogue/index/id_bib/2")]');
+  }
+}
+
+
+
+
+abstract class DomainsPerLibrariesCatalogueControllerAsAdminTatooineWithAccessSuppressionLimitTestCase extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->fixture(Class_UserGroup::class,
+                   ['id' => 4,
+                    'libelle' => 'Domains admins',
+                    'rights' => [Class_UserGroup::RIGHT_USER_DOMAINES_SUPPRESSION_LIMIT]]);
+
+    $admin = $this->fixture(Class_Users::class,
+                            ['id' => 9,
+                             'login' => 'tatooine',
+                             'password' => 'secret',
+                             'bib' => Class_Bib::find(2),
+                             'user_groups' => [Class_UserGroup::find(4)]])
+                  ->beAdminBib();
+    ZendAfi_Auth::getInstance()->logUser($admin);
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerAsAdminTatooineWithAccessCreatorIndexTest extends DomainsPerLibrariesCatalogueControllerAsAdminTatooineWithAccessSuppressionLimitTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/catalogue/index');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTableWithFolderLinkToPortal() {
+    $this->assertXPathContentContains('//table//td/a[contains(@href, "/admin/catalogue/index/id_bib/0")]',
+                                      'Portail');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTableWithFolderLinkToTatooine() {
+    $this->assertXPathContentContains('//table//td/a[contains(@href, "/admin/catalogue/index/id_bib/2")]',
+                                      'Tatooine');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsTableWithFolderLinkToAlderaan() {
+    $this->assertNotXPath('//table//td/a[contains(@href, "/admin/catalogue/index/id_bib/1")]');
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerAsAdminTatooineWithAccessCreatorAddTest extends DomainsPerLibrariesCatalogueControllerAsAdminTatooineWithAccessSuppressionLimitTestCase {
+  public function setUp() {
+    parent::setUp();
+
+    Class_Catalogue::findFirstBy(['libelle' => 'Tatooine characters'])
+      ->setUser(Class_Users::getIdentity())
+      ->save();
+
+    Class_Catalogue::findFirstBy(['libelle' => 'All characters'])
+      ->setUser(Class_Users::getIdentity())
+      ->save();
+
+    $this->dispatch('/admin/catalogue/add/id_bib/2');
+  }
+
+
+  /** @test */
+  public function selectParentIdShouldHaveOptGroupTatooine() {
+    $this->assertXPath('//select[@id="parent_id"]/optgroup[@label="Tatooine"]');
+  }
+
+
+  /** @test */
+  public function selectParentIdShouldHaveOptGroupPortail() {
+    $this->assertXPath('//select[@id="parent_id"]/optgroup[@label="Portail"]');
+  }
+
+
+  /** @test */
+  public function selectParentIdShouldNotHaveOptGroupAlderaan() {
+    $this->assertNotXPath('//select[@id="parent_id"]/optgroup[@label="Alderaan"]');
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerViewLibraryPortalTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/catalogue/index/id_bib/0');
+  }
+
+
+  /** @test */
+  public function tableForCategoriesTitleShouldBeListeDesDomainesAvecSousDomaines() {
+    $this->assertXPath('//table[@id="categories_Class_Catalogue"]/thead//th[text()="Liste des domaines avec sous-domaines"]');
+  }
+
+
+  /** @test */
+  public function tableForItemsTitleShouldBeListeDesDomaines() {
+    $this->assertXPath('//table[@id="items_Class_Catalogue"]/thead//th[text()="Liste des domaines"]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToSubDomainForAllCharacters() {
+    $this->assertXPathContentContains('//table[@id="categories_Class_Catalogue"]//tr//td//a[text()="All characters (1)"]/@href',
+                                      '/admin/catalogue/index/id_bib/0/id/2');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsDomainAllPlaces() {
+    $this->assertXPathContentContains('//table[@id="items_Class_Catalogue"]//tr//td', 'All places');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsDomainAlderaan() {
+    $this->assertNotXPathContentContains('//table//tr//td', 'Alderaan');
+  }
+
+
+  /** @test */
+  public function allPlacesDomainShouldHaveEditAction() {
+    $this->assertXPathContentContains('//table[@id="items_Class_Catalogue"]//div[@class="actions"]/a/@href',
+                                      '/admin/catalogue/edit/id_catalogue/1');
+  }
+
+
+  /** @test */
+  public function breadCrumbShouldHaveTwoEntries() {
+    $this->assertXPathCount('//div[@class="breadcrumb"]/a', 2);
+  }
+
+
+  /** @test */
+  public function breadCrumbShouldContainsPortalActionToAddNewDomain() {
+    $this->assertXPathContentContains('//div[@class="breadcrumb"]/div[@class="actions"]/a/@href',
+                                      '/admin/catalogue/add/id_bib/0');
+
+  }
+
+
+  /** @test */
+  public function pageShouldContainsSearchInput() {
+    $this->assertXPath('//input[@id="list_title_search"][@placeholder="libellé du domaine"]');
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerViewAllCharactersDomainTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/catalogue/index/id/2/id_bib/0');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsDomainWookies() {
+    $this->assertXPathContentContains('//table[@id="items_Class_Catalogue"]//tr//td', 'Wookiees');
+  }
+
+
+  /** @test */
+  public function pageShouldIndicatesEmptyCategories() {
+    $this->assertXPathContentContains('//table[@id="categories_Class_Catalogue"]//tr//td', 'Aucune donnée');
+  }
+
+
+  /** @test */
+  public function breadCrumbShouldContainsLinkToRoot() {
+    $this->assertXPathContentContains('//div[@class="breadcrumb"]/a[text()="Racine"]/@href',
+                                      '/admin/catalogue');
+
+  }
+
+
+  /** @test */
+  public function breadCrumbShouldContainsLinkToLibraryPortal() {
+    $this->assertXPathContentContains('//div[@class="breadcrumb"]/a[text()="Portail"]/@href',
+                                      '/admin/catalogue/index/id_bib/0');
+
+  }
+
+
+  /** @test */
+  public function breadCrumbShouldContainsLinkToDomainAllCharacters() {
+    $this->assertXPathContentContains('//div[@class="breadcrumb"]/a[text()="All characters"]/@href',
+                                      '/admin/catalogue/index/id_bib/0/id/2');
+
+  }
+
+
+  /** @test */
+  public function breadCrumbShouldContainsAllCharactersDomainActions() {
+    $this->assertXPathContentContains('//div[@class="breadcrumb"]/div[@class="actions"]/a/@href',
+                                      '/admin/catalogue/tester/id_catalogue/2');
+
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerViewAlderaanTownsDomainTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture(Class_Catalogue::class,
+                   ['id' => 10,
+                    'libelle' => 'Crevasse City',
+                    'library_id' => null,
+                    'parent_id' => 5]);
+
+    $this->dispatch('/admin/catalogue/index/id/5/id_bib/1');
+  }
+
+
+  /** @test */
+  public function breadCrumbShouldContainsLinkToLibraryAlderaan() {
+    $this->assertXPathContentContains('//div[@class="breadcrumb"]/a[text()="Alderaan"]/@href',
+                                      '/admin/catalogue/index/id_bib/1');
+
+  }
+
+
+  /** @test */
+  public function paseShouldDisplayDomainCrevasseCity() {
+    $this->assertXPath('//table[@id="items_Class_Catalogue"]//td[text()="Crevasse City"]',
+                       $this->_response->getBody());
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerPostDispatchInLibraryTatooineTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->postDispatch('/admin/catalogue/add/id_bib/2',
+                        ['libelle' => 'Tatooine Places',
+                         'parent_id' => '']);
+  }
+
+
+  /** @test */
+  public function domainWithLabelTatooinePlacesSevenShouldHaveBeenSaved() {
+    Class_Catalogue::clearCache();
+    $domain = Class_Catalogue::findFirstBy(['libelle' => 'Tatooine Places']);
+    $this->assertNotNull($domain);
+    return $domain;
+  }
+
+
+  /**
+   * @depends domainWithLabelTatooinePlacesSevenShouldHaveBeenSaved
+   * @test
+   */
+  public function tatooinePlacesShouldHaveLibraryIdTwo($domain) {
+    $this->assertEquals(2, $domain->getLibraryId());
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerPostDispatchInLibraryPortalTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->postDispatch('/admin/catalogue/add/id_bib/0',
+                        ['libelle' => 'Galaxies',
+                         'parent_id' => '']);
+  }
+
+
+  /** @test */
+  public function domainWithIdSixShouldHaveBeenSaved() {
+    Class_Catalogue::clearCache();
+    $this->assertNotNull(Class_Catalogue::find(6));
+  }
+
+
+  /** @test */
+  public function galaxiesShouldHaveLibraryIdNull() {
+    $this->assertNull(Class_Catalogue::find(6)->getLibraryId());
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerViewLibraryAlderaanTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/catalogue/index/id_bib/1');
+  }
+
+
+  /** @test */
+  public function breadCrumbShouldContainsAlderaanActionToAddNewDomain() {
+    $this->assertXPathContentContains('//div[@class="breadcrumb"]/div[@class="actions"]/a/@href',
+                                      '/admin/catalogue/add/id_bib/1');
+
+  }
+
+
+  /** @test */
+  public function pageShouldContainsDomainAlderaanPlaces() {
+    $this->assertXPathContentContains('//table[@id="categories_Class_Catalogue"]//tr[1]//td', 'Alderaan places');
+  }
+
+
+  /** @test */
+  public function alderanPlacesShouldDisplayDescriptionCoolPlaces() {
+    $this->assertXPathContentContains('//table[@id="categories_Class_Catalogue"]//tr[1]//td', 'Cool places on Alderaan');
+  }
+
+
+  /** @test */
+  public function alderanPlacesShouldDisplayCreatorAlderaanAdmin() {
+    $this->assertXPathContentContains('//table[@id="categories_Class_Catalogue"]//tr[1]//td', 'alderaan_admin');
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerEditDomainTest extends DomainsPerLibrariesTestCase {
+  /** @test */
+  public function formDataBackurlForWookiesShouldBeIndexIdTwo() {
+    $this->dispatch('/admin/catalogue/edit/id_catalogue/3');
+    $this->assertXPathContentContains('//form[contains(@action, "/admin/catalogue/edit/id_catalogue/3")]/@data-backurl',
+                                      '/admin/catalogue/index/id/2');
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerEditDomainAllPlacesTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/catalogue/edit/id_catalogue/1');
+  }
+
+
+  /** @test */
+  public function formDataBackurlShouldBeIndexIdBibZero() {
+    $this->assertXPathContentContains('//form[contains(@action, "/admin/catalogue/edit/id_catalogue/1")]/@data-backurl',
+                                      '/admin/catalogue/index/id_bib/0');
+  }
+
+
+  /** @test */
+  public function selectParentIdShouldHaveOptGroupAlderaanWithAlderaanPlaces() {
+    $this->assertXPath('//select[@id="parent_id"]/optgroup[@label="Alderaan"]/option[@value="4"][text()="Alderaan places"]');
+  }
+
+
+  /** @test */
+  public function selectParentIdShouldHaveOptGroupPortailWithAllCharacters() {
+    $this->assertXPath('//select[@id="parent_id"]/optgroup[@label="Portail"]/option[@value="2"][text()="All characters"]');
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerPostEditDomainAlderaanPlacesTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->postDispatch('/admin/catalogue/edit/id_catalogue/4',
+                        ['libelle' => 'Alderaan Places',
+                         'parent_id' => '']);
+  }
+
+
+  /** @test */
+  public function domainLibraryIdShouldStillBeOneForAlderaan() {
+    $this->assertEquals(1, Class_Catalogue::find(4)->getLibraryId());
+  }
+
+
+  /** @test */
+  public function responsShouldBeARedirect() {
+    $this->assertRedirectTo('/admin/catalogue/edit/id_catalogue/4');
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerPostSearchTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $_SERVER['HTTP_REFERER'] = 'http://monbokeh.org/admin/catalogue/index';
+    $this->postDispatch('/admin/catalogue/index/order/libelle/title_search/old_search',
+                        ['title_search' => 'places']);
+  }
+
+
+  /** @test */
+  public function responseShouldRedirectToTitleSearchPlaces() {
+    $this->assertRedirectTo('/admin/catalogue/index/title_search/places/order/libelle');
+  }
+
+
+  public function tearDown() {
+    unset($_SERVER['HTTP_REFERER']);
+    parent::tearDown();
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerSearchAllTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/catalogue/index/title_search/all');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsAllPlaces() {
+    $this->assertXPathContentContains('//table//tr//td', 'All places');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsAllCharactersAsCategory() {
+    $this->assertXPathContentContains('//table[@id="categories_Class_Catalogue"]//tr//td',
+                                      'All characters');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsWookies() {
+    $this->assertNotXPathContentContains('//table//tr//td', 'Wookies');
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerSearchCharactersInLibraryAlderaanTest extends DomainsPerLibrariesTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/catalogue/index/title_search/charac/id_bib/1');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsAlderaanCharactersAsItem() {
+    $this->assertXPathContentContains('//table[@id="items_Class_Catalogue"]//tr//td', 'Alderaan characters');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsAlderaanCharactersShouldHaveLinkToAlderaanPlaces() {
+    $this->assertXPathContentContains('//table//tr//td/span[text()="Alderaan characters"]/following-sibling::p/a[text()="Alderaan places"]/@href', '/admin/catalogue/index/id_bib/1/id/4');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsAllCharacters() {
+    $this->assertNotXPathContentContains('//table//tr//td', 'All characters');
+  }
+
+
+  /** @test */
+  public function breadCrumbShouldContainsLinkToLibraryAlderaan() {
+    $this->assertXPathContentContains('//div[@class="breadcrumb"]/a[text()="Alderaan"]/@href',
+                                      '/admin/catalogue/index/id_bib/1');
+
+  }
+}
+
+
+
+
+class DomainsPerLibrariesCatalogueControllerJsonRendersTest extends DomainsPerLibrariesTestCase {
+  /** @test */
+  public function domainesPaniersShouldContainsDomainsPerLibraries() {
+    $this->onLoaderOfModel(Class_PanierNotice::class)
+         ->whenCalled('findAllBelongsToAdmin')
+         ->answers([]);
+
+    $this->dispatch('admin/catalogue/domaines-paniers-json', true);
+    $datas = json_decode($this->_response->getBody(), true);
+
+    $item_options = ['ico' => Class_Url::baseUrl() . '/public/admin/images/picto/domaines_16.png'];
+
+    $library_options = ['ico' => Class_Url::baseUrl() . '/public/admin/images/picto/bibliotheques_16.png',
+                        'removeCheckbox' => true];
+    $this->assertEquals([['id' => 'panier_for_user',
+                          'label' => 'Mes paniers',
+                          'categories' => [],
+                          'items' => [],
+                          'options' => ['multipleSelection' => false]],
+
+                         ['id' => 'domaines_paniers',
+                          'label' => 'Domaines',
+                          'categories' => [['id' => 'Alderaan',
+                                            'label' => 'Alderaan',
+                                            'categories' => [[
+                                                              'id' => 6,
+                                                              'label' => 'Alderaan characters',
+                                                              'categories' => [],
+                                                              'items' => [],
+                                                              'options' => $item_options],
+
+                                                             ['id' => 4,
+                                                              'label' => 'Alderaan places',
+                                                              'categories' => [[
+                                                                                'id' => 6,
+                                                                                'label' => 'Alderaan characters',
+                                                                                'categories' => [],
+                                                                                'items' => [],
+                                                                                'options' => $item_options],
+
+                                                                               ['id' => 5,
+                                                                                'label' => 'Alderaan towns',
+                                                                                'categories' => [],
+                                                                                'items' => [],
+                                                                                'options' => $item_options]],
+                                                              'items' => [],
+                                                              'options' => $item_options],
+
+                                                             ['id' => 5,
+                                                              'label' => 'Alderaan towns',
+                                                              'categories' => [],
+                                                              'items' => [],
+                                                              'options' => $item_options]],
+                                            'items' => [],
+                                            'options' => $library_options],
+
+                                           ['id' => 'Portail',
+                                            'label' => 'Portail',
+                                            'categories' => [['id' => 2,
+                                                              'label' => 'All characters',
+                                                              'categories' => [[
+                                                                                'id' => 3,
+                                                                                'label' => 'Wookiees',
+                                                                                'categories' => [],
+                                                                                'items' => [],
+                                                                                'options' => $item_options]],
+                                                              'items' => [],
+                                                              'options' => $item_options],
+
+                                                             ['id' => 1,
+                                                              'label' => 'All places',
+                                                              'categories' => [],
+                                                              'items' => [],
+                                                              'options' => $item_options],
+
+                                                             ['id' => 3,
+                                                              'label' => 'Wookiees',
+                                                              'categories' => [],
+                                                              'items' => [],
+                                                              'options' => $item_options]],
+                                            'items' => [],
+                                            'options' => $library_options],
+
+                                           ['id' => 'Tatooine',
+                                            'label' => 'Tatooine',
+                                            'categories' => [[
+                                                              'id' => 7,
+                                                              'label' => 'Tatooine characters',
+                                                              'categories' => [],
+                                                              'items' => [],
+                                                              'options' => $item_options]],
+                                            'items' => [],
+                                            'options' => $library_options]],
+                          'items' => [],
+                          'options' => ['ico' => Class_Url::baseUrl() . '/public/admin/images/picto/domaines_16.png',
+                                        'multipleSelection' => false]],
+
+                         ['id' => 'paniers_by_users',
+                          'label' => 'Autres paniers',
+                          'categories' => [],
+                          'items' => [],
+                          'options' => ['multipleSelection' => false]]
+                         ],
+                        $datas);
+  }
+
+
+  /** @test */
+  public function browsableDomainsShouldContainsDomainsPerLibraries() {
+    $this->dispatch('admin/catalogue/browsable-domains', true);
+
+    $datas = json_decode($this->_response->getBody(), true);
+    $item_options = ['ico' => (Class_Url::baseUrl() . '/public/admin/images/picto/domaines_16.png'),
+                     'multipleSelection' => false];
+
+    $this->assertEquals([['id' => 'Alderaan',
+                          'label' => 'Alderaan',
+                          'categories' => [['id' => 6,
+                                            'label' => 'Alderaan characters',
+                                            'categories' => [],
+                                            'items' => [],
+                                            'options' => $item_options],
+
+                                           ['id' => 4,
+                                            'label' => 'Alderaan places',
+                                            'categories' => [],
+                                            'items' => [['id' => 6,
+                                                         'label' => 'Alderaan characters',
+                                                         'options' => $item_options],
+
+                                                        ['id' => 5,
+                                                         'label' => 'Alderaan towns',
+                                                         'options' => $item_options]],
+                                            'options' => $item_options],
+
+                                           ['id' => 5,
+                                            'label' => 'Alderaan towns',
+                                            'categories' => [],
+                                            'items' => [],
+                                            'options' => $item_options]],
+                          'items' => [],
+                          'options' => []],
+
+
+                         ['id' => 'Portail',
+                          'label' => 'Portail',
+                          'categories' => [['id' => 2,
+                                            'label' => 'All characters',
+                                            'categories' => [],
+                                            'items' => [['id' => 3,
+                                                         'label' => 'Wookiees',
+                                                         'options' => $item_options]],
+                                            'options' => $item_options],
+
+                                           ['id' => 1,
+                                            'label' => 'All places',
+                                            'categories' => [],
+                                            'items' => [],
+                                            'options' => $item_options],
+
+                                           ['id' => 3,
+                                            'label' => 'Wookiees',
+                                            'categories' => [],
+                                            'items' => [],
+                                            'options' => $item_options]],
+                          'items' => [],
+                          'options' => []],
+
+
+
+                         ['id' => 'Tatooine',
+                          'label' => 'Tatooine',
+                          'categories' => [[
+                                            'id' => 7,
+                                            'label' => 'Tatooine characters',
+                                            'categories' => [],
+                                            'items' => [],
+                                            'options' => $item_options]],
+                          'items' => [],
+                          'options' => []]
+                         ],
+                        $datas);
+  }
+}
diff --git a/tests/scenarios/Journal/JournalTest.php b/tests/scenarios/Journal/JournalTest.php
index c357b3cd42c35233bf7db3bc3da2c844c5b97d06..c85b08b8cae53ef993e6fd8ef92c0af9494ad963 100644
--- a/tests/scenarios/Journal/JournalTest.php
+++ b/tests/scenarios/Journal/JournalTest.php
@@ -428,7 +428,7 @@ class JournalCatalogueEditTest extends JournalTestCase {
 
   /** @test */
   public function journalDetailsShouldContainsNewValue() {
-    $this->assertEquals('{"parent_id":null,"libelle":"Coups de coeur","oai_spec":"","description":"les coups de coeur de l\'equipe","bibliotheque":"","section":"","genre":"","langue":"","annexe":"","emplacement":"","auteur":"","matiere":"","dewey":"","pcdm4":"","thesaurus":"","tags":"","interet":"","type_doc":1,"annee_debut":"","annee_fin":"","cote_debut":"","cote_fin":"","nouveaute":"","indexer":0,"url_img":"","custom_form_id":null,"custom_form_values":"","id":1,"id_catalogue":1}',
+    $this->assertEquals('{"parent_id":null,"libelle":"Coups de coeur","oai_spec":"","description":"les coups de coeur de l\'equipe","bibliotheque":"","section":"","genre":"","langue":"","annexe":"","emplacement":"","auteur":"","matiere":"","dewey":"","pcdm4":"","thesaurus":"","tags":"","interet":"","type_doc":1,"annee_debut":"","annee_fin":"","cote_debut":"","cote_fin":"","nouveaute":"","indexer":0,"url_img":"","custom_form_id":null,"custom_form_values":"","library_id":null,"id":1,"id_catalogue":1}',
                         Class_JournalDetail::findFirstBy(['type' => 'new_value'])->getValue());
   }