Сегодня расскажу очередную интересную мини-историю из цикла разработки JMatrixPlatform. В прошлой серии я показал как элегантно удалось решить проблему загрузки исторических данных и справочников. В этот раз поговорим о многомодульной навигации в UI. Или как удачная архитектура открыла новые возможности, которые я раньше просто не замечал.

Проблема

Во всех мультимодульных системах существует весьма распространённая "боль", связанная с навигацией. Когда в одной системе реализуются десятки, а то и сотни модулей и подсистем, которые отображаются, в зависимости от ролевого доступа текущего пользователя, рано или поздно это приводит к тому, что главная страница становится перегруженной различными ярлыками, категориями, подвкладками и кнопками, в которых даже администратору бывает сложно ориентироваться. Не так легко вспомнить, что и где надо щёлкнуть, чтобы перейти в модуль документооборота или модуль управления проектами.

Чтобы хоть как-то организовать этот хаос, приходится группировать все элементы в соответствующие разделы, ярлыки и т.д. В результате найти, куда запрятан тот или иной функционал - нереально. В ход идут инструкции, которые, кстати, тоже надо сначала найти.

Если пользователь имеет доступ только к паре модулей - навигация по UI не проблема, всё легко запоминается, но когда у пользователя десяток рабочих процессов, начинаются сложности. Абстрактный пример:

Главная страница -> MyActions -> Управление требованиями -> Распределение требований

Главная страница -> MyDesk -> Документация -> Учёт требований

Главная страница -> MyDesk -> Отчёты -> Нераспределённые требования -> Сформировать

Главная страница -> MyActions -> Текущий пользователь -> Мой профиль

До сегодняшнего дня я не слышал ни одного удачного решения этой проблемы, все просто смирились. Каждый знает свою часть и прекрасно ориентируется только в ней. Шаг влево, шаг вправо - полная темнота. Поэтому, даже при постановке задач на разработку, обязательным первым пунктом является указание пути навигации до нужного компонента, как в примере выше. Тоже самое касается и инструкций: "откройте главную страницу, раскройте слева категорию A, в ней перейдите в модуль B, далее во вкладку C, в которой нажмите D" - классика!

Его величество VSCode

Тем временем JetBrains ушёл из РФ. Я решил попробовать VSCode Extension Pack for Java. Там я и увидел палитру команд.

Кто не знаком - это такая "командная строка", с быстрым переходом в неё по сочетанию клавиш, с поиском и вызовом любой функции IDE. Набираешь любую команду и "вуаля" - она сразу доступна к выполнению:

Скриншот с официального сайта VSCode
Скриншот с официального сайта VSCode

Не знаю, как вам, но по мне - это гениально! Сразу всплыли в голове все эти часы бесполезных обсуждений где разместить очередную категорию, кнопку, вкладку или модуль: "Вот тут неудобно, давайте там, а там будет не видна, потому что меню доступно только в солнечную погоду...". Особенно бесили кнопки, доступные только администратору (обновить кэш, запустить синхронизацию, настройки, параметры, отчёт по входам и т.д.). Разместить их можно где угодно, но вот запомнить где - та ещё задачка.

VSCode показал прорывную идею - единую строку поиска команд, которая даёт быстрый доступ к любой функции IDE. Не знаю, как они это реализовали технически, но похоже, что в JMatrixPlatform я могу сделать нечто подобное. Как оказалось у меня "под носом" лежит вся необходимая архитектура. И до этого момента, мне не приходила в голову идея воспользоваться ей.

То что лежит под носом

Напомню, что JMatrixPlatform состоит из маленьких деталек, из которых затем собирается весь модуль. Даже главная страница собирается из таких деталек в виде кнопок-команд. Все категории и подкатегории слева, тулбар сверху справа, панели инструментов таблицы и т.д. - всё это отдельные кастомизируемые кнопки-команды:

Главная страница
Главная страница

Так выглядит кастомизация команды Проекты (в категории Документооборот, см. скриншот выше):

@JModelPart
public final class ACDProjectsDesk extends JCommand {
  public static ACDProjectsDesk COMMAND = new ACDProjectsDesk();

  @Override
  public JHref getHref(JContext ctx, JRequestParams requestParams) {
    return new JHrefTable(
        ATBProjectsDesk.TABLE,
        JTarget.CONTENT,
        Actions.TOOLBAR,
        null,
        AIQProjectsAll.INQUIRY,
        JHrefTable.RowSelection.MULTI);
  }
}

Вся локализация UI размещается в соответствующих файлах messages_ru.properties:

ru.demo.matrix.schema.ui.command.ACDProjectsDesk=Проекты

Т.е. в платформе уже используется архитектура, где на любое UI действие требуется создать соответствующую команду. И чтобы реализовать палитру команд, через верхний поиск, достаточно написать простейший Java контроллер, который просто вернёт весь список команд, доступных пользователю, с последующим поиском по name, label или description, на стороне UI. И для этого даже напрягаться не потребуется.

Единственное, чего не хватало в этом решении - это намеренного ограничения "допуска" некоторых команд в палитру (например создание замечания осуществляется только в конкретном документе, соответственно эта кнопка бесполезна в палитре) и отсутствие механизма организации семантического поиска по синонимам (кнопка называется "компании", а пользователь ищет "организации"). На помощь опять приходят хэштеги, которые решают одним выстрелом обе проблемы одновременно: в палитру допускаются только команды с указанными хэштегами, а сами хэштеги реализуют простой механизм расширения возможностей поиска, с помощью синонимов и дополнительного контекста, который не добавить в label или description:

@JModelPart
public final class ACDProjectsDesk extends JCommand {
  //...

  @Override
  public Set<String> getHashTags() {
    return Set.of(
      "документооборот",
      "демонстрация",
      "проекты",
      "комплекты");
  }
}

Контроллер:

  @GetMapping("/navigator/paletteCommands")
  public ResponseEntity<List<JDTOCommand>> getCommands(
      @JPathContextVariable JContext ctx,
      @RequestParam Map<String, String> requestParamsMap) {
    
    JRequestParams requestParams = new JRequestParams(requestParamsMap);

    Set<JCommand> commands = JModel.getAdminListByClass(JCommand.class);

    // В палитру попадают только команды с хэштегами.
    //
    // Контекстные команды (Удалить, Изменить статус и т.п.)
    // в глобальном поиске бесполезны — им нужен выделенный объект.
    //
    // Хэштеги — явная маркировка навигационных команд.
    List<JDTOCommand> results = commands
        .stream()
        .filter(cmd -> cmd.getHashTags() != null && !cmd.getHashTags().isEmpty() && cmd.show(ctx, requestParams))
        .map(cmd -> new JDTOCommand(ctx, requestParams, cmd))
        .toList();

    return ResponseEntity.ok(results);
  }

Изначально сделал поиск сразу на стороне контроллера, но не понравился отклик. Учитывая, что количество доступных команд не ожидается более 1тыс. - сделал фильтрацию на стороне UI. Отклик теперь мгновенный! А метод фильтрации в контроллере пока полежит до "лучших" времён.

Пример json, который возвращает контроллер:

[
   {
      "name":"ACPersonDesk",
      "description":"Реестр пользователей платформы",
      "label":"Пользователи",
      "icon":null,
      "settings":{},
      "href":{
         "url":"../table/ATBPersonDesk",
         "requestParams":{
            "toolbar":"AMPersonDeskTableActions",
            "rowSelection":"MULTI",
            "inquiry":"AIQPersonsDesk",
            "target":"CONTENT"
         }
      },
      "hashTags":["пользователи", "структурапредприятия"]
   },
   {
      "name":"ACDZfReferencesDesk",
      "description":null,
      "label":"Справочники",
      "icon":null,
      "settings":{},
      "href":{
         "url":"../portal/ACDZfReferencesDesk$Portal",
         "requestParams":{
            "target":"CONTENT"
         }
      },
      "hashTags":["zf", "справочники"]
   },
  ...
]

Чуть-чуть модифицируем строку поиска на Vue и получаем отличный удобный инструмент, который ищет все доступные команды.

Смело? Смело! Пробуем.

Вместо ручной навигации Главная страница -> Документооборот -> Проекты вводим "проекты" и вот они:

Категория проекты
Категория проекты

Или вместо Главная страница -> Документооборот -> Чертежи -> Отчёты -> Контроль разработки вводим "контроль" и сразу получаем возможность сразу выгрузить нужный отчёт. Если по слову "контроль" будет найдено несколько команд, то хэштеги и всплывающее описание дадут дополнительный контекст для выбора нужной команды.

При этом не важно, что делает команда - открывает компонент или сразу выполняет действие. Палитра команд - это быстрый поиск по контексту и синонимам. Чем точнее указаны хэштеги, тем выше шанс, что пользователь сразу найдёт нужную команду, где бы она ни была спрятана.

Заключение

На всю реализацию ушло всего полдня. Архитектура платформы, где любое действие - это отдельная команда с именем, описанием и правами доступа, сделала реализацию тривиальной задачей. При этом командная палитра стала естественным расширением платформы, а не отдельной надстройкой с отдельной конфигурацией.

Важно понимать: палитра команд - не замена классическому UI, а дополнительный канал навигации. У меня нет цели заменить главную страницу одной строкой поиска. В консервативных инжиниринговых компаниях никто не откажется от привычных меню. Но возможность быстро найти нужную функциональность без инструкции оценят многие. А для административных функций (очистка кэша, синхронизация, отчёты) палитра навсегда снимает вопрос "куда это повесить в UI".

Если вы делаете отдельные моносистемы (на разных DNS) - палитра вам не нужна. Если же ваша система выросла в экосистему из сотен модулей - вы столкнётесь с той же болью. И тогда, возможно, вспомните эту статью.

На сегодня всё. Всем ярких палитр!