Библиотечные проекты
Кастомизация табличных библиотечных проектов

Кастомизация табличных форм библиотечного проекта

Порой необходимо уметь кастомизировать свой библиотечный проект под нужды заказчика. Например скрывать или отображать те или иные колонки, пункты меню. Отображать или скрывать баннер стандартный формы, указывать отображение тех параметров которые не заложены стандартной процедурой и прочее

Ниже приведен список параметров которые необходимо учесть в Вашем библиотечном проекте для достижения возможностей его кастомизации. Пример был собран на основе опыта команды qagr и может быть пересмотрен командой q.palette в дальнейшем.

В дальнейшем возможно более автоматизированное решение.

Список параметров

Ниже описаны поля изменение которых внутри проекта позволит менять различне поля в Вашем библиотечном проекте

{  
   myPbcConfig: 
  {
     service: 'myservice',             // параметр для перенаправления запросов к бэку к своему сервису
     isBannerVisible: false,           // Отображает или скрывает баннер библиотечного проекта.
     customOnClickAdd: true,           // Позволяет открыть кастомную форму вместо стандартной при клике на кнопку "Добавить". Позволяет инициировать                                        собственное событие
                                       // прикладной компонент подписывается на это событие и открывает свою форму. О подписке на событие можно почитать здесь:
                                       // https://qpalette-dev.diasoft.ru/interface/qpalettedocui/documentation-12/development/qpalette-components/q-web-component#interaction_recieve
     customOnСlickEdit: true, // Позвляет открыть кастомную форму при клике на кнопку "Редактировать"
                                       // если передан, то вместо открытия стандартной формы редактирования будет инициировано событие ваше собственное событие
     //Похожим образом кастомизируются другие кнопки, которые содержатся в Вашем библиотечном проекте
 
     rightMenu: [                      // параметр для управления существующими пунктами контекстного меню, доступных по кнопке троеточия 
      {
        id: 'yourId',               // идентификатор пункта меню. Существующие идентификаторы определяются Вашим библиотечным проектом.
        caption: 'Мой заголовок',   // заголовок пункта меню, можно передать свой
        visible: true               // признак видимости (отображает или скрывает пункт меню)
        eventName: 'click-on-id'    // необязательный параметр, если передан, то при нажатии на этот пункт меню вместо стандартного действия  будет инициировано событие  
      },                            // через QEventsService с переданным именем, в параметре payload.agreement придут все данные по договору из той строки, где было вызвано меню
       {
        id: 'someHiddenLine',
        caption: 'Скрытый пункт',
        visible: false                // Пример скрытия того или иного пункта меню
      },
      {
        id: 'newItem',                // Пример добавления нового пункта меню с идентификатором newItem
        caption : 'Новый пункт меню',
        visible: true,
        eventName: 'click-on-new-item' // при нажатии на этот пункт меню будет инициировано событие через QEventsService, в параметре payload.agreement придут все данные по договору
      }
      
    ],
    tableColumns: [                     //параметр для управления столбцами таблицы на форме должен быть с типом TableHeader[] (cм. описание ниже)
      {                                 //необходимо указать id для управления существующими столбцами, существующие идентификаторы: 'number', 'product', 'part1', 'part2', 
        id: 'number',                   // 'isOffer', 'dateStart', 'dateEnd', 'state'
        number: 1                       // чтобы поменять расположение столбцов, задаем всем видимым столбцам номер (number), столбцы будут отсортированы по возрастанию этого параметра
      },
      {
        id: 'product',
        header: 'Продукт',            // если указать header - столбец буде переименован
        number: 2
      },
      {
        id: 'part1',
        header: 'Сторона 1',      
        number: 3
      },
      {
        id: 'part2',
        header: 'Сторона 2',
        number: 4
      },
      {
        id: 'isOffer',      
        visible: false              // если установить признак  visible: false, столбец будет скрыт   
      },
      {
        id: 'dateStart', 
        number: 5
      },
      {
        id: 'dateEnd',      
        number: 6
      },
      {
        id: 'state',
        number: 8      
      },
      {
        id: 'newField1',            // если добавить новые идентификаторы - новый столбец будет добавлен на форму 
        header: 'Новый столбец',    // с указанным заголовком  
        field: 'newField',          // field - поле из апи для вывода данных в новом столбце
        number: 7
      }
    ]
  } 
 
  tableFilters = [                 // параметр для управления фильтрами таблицы на форме договоров должен быть с типом TableFilter[] (cм. описание ниже)
    {                              // обязательно указать id для управления существующими столбцами, существующие идентификаторы: 'client', 'number', 'product',  
      id: 'client',                // 'dateStart', 'dateEnd', 'state'
      label: 'Сторона 2',          // если указать label - фильтр будет переименован
      number: 2                    // чтобы поменять расположение фильтров, задаем всем видимым фильтрам номер (number), они будут отсортированы по возрастанию этого параметра 
    },
    {
      id: 'number',
      label: 'Наименование',      
      number: 1
    },
    {
      id: 'product',
      label: 'Продукт',      
      number: 3
    },
    {
      id: 'dateStart',
      label: 'Дата начала',      
      number: 4
    },
    {
      id: 'dateEnd',      
      visible: false              // если установить признак  visible: false, фильтр будет скрыт
    },
    {
      id: 'state',      
      visible: false
    },
    {
      id: 'newFilter',          // если добавить новые идентификаторы - новый фильтр будет добавлен на форму  
      label: 'Новый фильтр',    // с указанным заголовком 
      type: 'input',            // поддержка типов определяется на этапе разработки библиотечного проекта
      field: 'number',          // field - поле из апи для фильтрации по этому параметру 
      number: 5
    }
  ],
 
{
     isExpanded = true,                        //параметр для "раскрытия строки", если true - строка будет раcкрываться, показывая дополнительную информацию по договору
     expandedLabel = 'Расширенная информация', //общий заголовок для "раскрытой строки"
     expandedCols = [                          //набор столбцов для раскрытой строки
      {
        caption: 'ID договора',                //заголовок 
        field: 'agreementId',                  //поле из апи для вывода информации
        colspan: 3                             //ширина ячейки на сетке из 12 полей
      },                                       //если указать 12 - столбец займет всю ширину строки, если указать 6 - столбец займет половину ширины строки                   
      {                                        //если указать 3 - в строке уместятся 4 такие колонки и т.д.
        caption: 'Номер договора',
        field: 'number',
        colspan: 3
      },
      {
        caption: 'Обслуживающая сторона',
        field: 'servingPartyName',
        colspan: 3
      },
      {
        caption: 'Обслуживаемая сторона',
        field: 'servedPartyName',
        colspan: 3
      },
      {
        caption: 'Описание',
        field: 'description',
        colspan: 12
      }
     ]
}
 

Описание интерфейсов

export class TableHeader {
    id: string;
    field?: string;
    header?: string;
    visible?: boolean;
    number?: number;
}
 
export class TableFilter {
    id: string;
    label?: string;
    type?: string;
    visible?: boolean;
    lookupField?: string;
    remoteWidgetComponentUrl?: string;
    widgetComponentSelector?: string;
    field?: string;
    options?: any;
    optionLabel?: string;
    optionValue?: string;
    number?: number  
}

Посмотреть реализацию данного подхода можно тут (opens in a new tab)

Список изменяемых параметров:

  • Скрытие / отображение компонента баннер. Возможность добавления собственного компонента Баннер из библиотеки Q.Palette и как следствие изменение расчетных показателей внутри баннера на табличной форме
  • Возможность изменения пунктов контекстного меню таблицы
    • Скрытие или отображение пунктов меню
    • Измненение стандартного состояния пунктов меню
    • Измненеие текста пунктов меню
    • Изменение стандартного состава пунктов меню (Добавление своих пунктов)
  • Возможность отображения собственных форм не входящих в состав библиотечного проекта
  • Кастомизация таблиц (списков)
    • Изменение наименований столбцов
    • Изменение порядка столбцов
    • Добавление новых столбцов
    • Отключение столбцов
    • Включение и отключение параметра раскрытия строки
    • Изменение наименований атрибутов внутри раскрытой строки
    • Изменение порядка атрибутов внутри раскрытой строки
    • Добавление атрибутов внутри раскрытой строки
    • Скрытие внутри раскрытой строки
  • Кастомизация фильтров
    • Изменение наименований фильтров
    • Изменение порядка фильтров
    • Добавление новых фильтров
    • Отключение фильтров
    • Скрытие доступных по умолчанию фильтов

Пример кода кастомизированной страницы

<section class="p-mb-3" *ngIf="isBannerVisible">
    <q-banner [expanded]="true" caption="Договоры обслуживания" picture="/assets/banner-pics/banner-test-02.jpg" style="margin-bottom: 1rem;">
        <q-banner-block subtitle="Действующих договоров" caption="{{cnt_agr_act}}" badgeText="{{cnt_agr_act_plus}}" badgeColor="success"></q-banner-block>
        <q-banner-block subtitle="В процессе обработки" caption="{{cnt_agr_proc}}" badgeText="{{cnt_agr_proc_plus}}" badgeColor="warning"></q-banner-block>
    </q-banner>
</section>
 
<p-card styleClass="p-card--no-padding">
    <ng-template pTemplate="header">
        <div class="p-fluid p-d-flex p-flex-wrap p-flex-row p-jc-start p-ac-start p-d-flex--gap-2">
          <div class="q-filter__wrapper p-d-flex p-jc-between q-gap-4 p-grow-1">
            <div class="q-filter__container p-d-flex p-flex-wrap q-gap-2 p-grow-1">
              <q-filter-wrapper *ngFor="let filter of _selectedFilters"
                label="{{filter.label}}"
                (clear)="onClearFilter(filter); getAgreementList(undefined, true)"
              >
                <q-lookup *ngIf="filter.type == 'lookup'"
                  lookupField="{{filter.lookupField}}"
                  remoteWidgetComponentUrl="{{filter.remoteWidgetComponentUrl}}"
                  widgetComponentSelector="{{filter.widgetComponentSelector}}"
                  displayMode="modal"
                  size="small"
                  (selectedItem)="onSelectLookup($event, filter)"
                >
                </q-lookup>
                <input *ngIf="filter.type == 'input'"
                  id="{{filter.id}}"
                  type="text"
                  pInputText
                  [(ngModel)]="tableFilters[filter.field]"
                  (ngModelChange)="getAgreementList(undefined, true)"
                />
                <p-calendar *ngIf="filter.type == 'calendar'"
                  id="{{filter.id}}"
                  dateFormat="dd.mm.yy"
                  [(ngModel)]="tableFilters[filter.field]"
                  (ngModelChange)="getAgreementList(undefined, true)"
                ></p-calendar>
                <p-multiSelect *ngIf="filter.type == 'multiselect'"
                  styleClass="p-multiSelect-cust p-multiSelect-cust--filter-status"
                  [options]="filter.options"
                  [(ngModel)]="tableFilters[filter.field]"
                  [showHeader]="false"
                  [showToggleAll]="false"
                  [filter]="false"
                  [maxSelectedLabels]="2"
                  (onChange)="getAgreementList(undefined, true)"
                  optionLabel="{{filter.optionLabel}}"
                  optionValue="{{filter.optionValue}}"
                ></p-multiSelect>
              </q-filter-wrapper>
            </div>
            <div class="q-filter__actions p-d-flex p-jc-end q-gap-2">
              <button
                (click)="getAgreementList(undefined, true)"
                pButton
                class="p-button-sm p-button-raised p-button-text"
                label=""
                icon="pi pi-refresh"
                iconPos="left"
              ></button>
              <button
                (click)="op.toggle($event)"
                pButton
                class="p-button-sm p-button-text p-button-raised"
                icon="pi pi-filter"
                iconPos="left"
                pTooltip=""
              ></button>
              <button
                pButton
                class="p-button-sm p-button-md p-button-primary"
                label="{{ 'ACTION.ADD' | translate }}"
                (click)="addAgreement()"
                iconPos="left"
              ></button>
              <p-overlayPanel #op appendTo="body">
                <ng-template pTemplate>
                  <p-listbox
                    [options]="filters"
                    [filter]="false"
                    [(ngModel)]="selectedFilters"
                    [multiple]="true"
                    [checkbox]="true"
                    optionLabel="label"
                    pTooltip=""
                  >
                  </p-listbox>
                </ng-template>
              </p-overlayPanel>
 
            </div>
          </div>
        </div>
    </ng-template>
 
<p-table
[columns]="_selectedColumns"
[value]="agreementList"
[paginator]="true"
class="p-table"
[rows]="tableParams.count"
[first]="tableParams.first"
[sortField]="tableParams.sortField"
[sortOrder]="tableParams.sortOrder"
[totalRecords]="tableParams.recordsCount"
[loading]="loading"
[lazy]="true"
styleClass="p-datatable-sm"
[paginator]="true"
(onLazyLoad)="log('onLazyLoad'); getAgreementList($event)"
[rowsPerPageOptions]="[10, 25, 50]"
currentPageReportTemplate="Показаны {first} - {last} из {totalRecords} записей"
[showCurrentPageReport]="true"
[rowHover]="true"
dataKey="agreementId"
[reorderableColumns]="true"
[styleClass]="sizeClass"
>
<ng-template pTemplate="header" let-columns let-expanded="expanded">
  <tr>
    <th style="width: 3rem; flex: 0 0 auto" *ngIf="isExpanded"></th>
 
    <th *ngFor="let col of columns" [pSortableColumn]="col.field" pReorderableColumn>
      <div style="font-size: 13px">
        {{ col.header }}
        <p-sortIcon [field]="col.field"></p-sortIcon>
      </div>
    </th>
    <th style="text-align: right" [ngStyle]="{ width: '155px' }">
      <button
        #menuToggler
        (click)="tableMenu.toggle($event)"
        pButton
        class="p-button-xs p-button-text p-button-raised"
        icon="pi pi-sliders-v"
        iconPos="left"
      ></button>
 
      <p-tieredMenu
        #tableMenu
        [popup]="true"
        [model]="tableMenuItems"
        appendTo="body"
      ></p-tieredMenu>
      <p-dialog
        header="Отображение колонок"
        [(visible)]="colsToggler"
        [style]="{ width: '20rem' }"
        [contentStyle]="{padding: '2'}">
 
        <p-listbox
          [options]="cols"
          [(ngModel)]="selectedColumns"
          [metaKeyselection]="false"
          [checkbox]="true"
          [multiple]="true"
          optionLabel="header"
        >
          <ng-template let-col pTemplate="item">
            <div class="country-item">
              <div>{{ col.header }}</div>
            </div>
          </ng-template>
 
        </p-listbox>
      </p-dialog>
    </th>
  </tr>
</ng-template>
 
<ng-template pTemplate="body" let-item let-columns="columns" let-index="rowIndex" let-expanded="expanded">
  <tr [pReorderableRow]="index">
    <td *ngIf="isExpanded">
      <button
        type="button"
        pButton
        [pRowToggler]="item"
        class="p-button-text p-button-rounded p-button-plain p-button-xs"
        [icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"
      ></button>
    </td>
    <td style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0.55em" *ngFor="let col of columns">
      <span title="{{ item[col.field] }}" *ngIf="!['isOffer', 'stateName', 'dateStart', 'dateEnd'].includes(col.field)">
        {{ item[col.field] }}
      </span>
      <span title="{{ item[col.field] }}" *ngIf="['dateStart', 'dateEnd'].includes(col.field)">
        {{ item[col.field] | date:'dd.MM.yyyy' }}
      </span>
      <span title="{{ item[col.field] }}" *ngIf="col.field == 'isOffer'">
        {{ getOfferValue(item.isOffer) }}
      </span>
      <span title="{{ item[col.field] }}" *ngIf="col.field == 'stateName'">
        <p-badge
          value="{{ item.stateName }}"
          styleClass="{{ getStateColor(item.stateName) }}"
        >
        </p-badge>
      </span>
    </td>
 
    <td [ngClass]="{ 'p-text-right': true }">
      <div
        class="p-fluid p-d-flex p-flex-row p-jc-end p-ac-end p-d-flex--gap-1 p-mt-0 p-mr-0 p-mb-0 p-ml-0 p-pt-0 p-pr-0 p-pb-0 p-pl-0"
      >
        <button
          pButton
          pTooltip="{{ 'ACTION.EDIT' | translate }}"
          (click)="onEditAgreement(item)"
          class="p-button-xs p-button-text p-button-raised"
          icon="pi pi-pencil"
          iconPos="left"
        ></button>
        <button
          pButton
          class="p-button-xs p-button-text p-button-raised"
          (click)="$event.stopPropagation(); onDeleteAgreement(item)"
          pTooltip="{{ 'ACTION.DELETE' | translate }}"
          tooltipPosition="left"
          icon="pi pi-trash"
          iconPos="left"
        ></button>
 
        <div
          #menudiv
          [ngStyle]="{position: 'fixed', top: 0, left: 0, 'z-index': 2000}"
          (mouseover)="visible = true"
          (mouseleave)="visible = false; tableMenu.hide()"
        ></div>
        <p-tieredMenu
          styleClass="p-tieredmenu-cust p-tieredmenu-cust--w20 p-tieredmenu-cust--submenu-left"
          #tableMenu
          [model]="menuItems"
          [appendTo]="menudiv"
          [popup]="true">
        </p-tieredMenu>
        <button
          pButton
          id="btn-tiered-menu"
          class="p-button-xs p-button-text p-button-raised"
          (click)="setupMenuItems(item); tableMenu.toggle($event)"
          (mouseleave)="pauseMenu(300)"
          icon="pi pi-ellipsis-h"
          iConPos="left"
        ></button>
      </div>
    </td>
  </tr>
</ng-template>
<ng-template pTemplate="rowexpansion" let-item>
  <tr>
    <td colSpan="{{_selectedColumns.length+2}}">
      <div class="p-fluid p-grid">
        <div class="p-col p-lg-12 p-md-12 p-col-12" *ngIf="(expandedLabel != '')">
          <p>
              <span class="p-text-left">
                <b> {{expandedLabel}} </b>
              </span>
          </p>
        </div>
 
        <div class="p-col p-lg-{{expCol.colspan}} p-md-{{expCol.colspan}} p-col-12" *ngFor="let expCol of expandedCols">
          <q-info label="{{expCol.caption}}">
            <ng-template>
              <p class="info-text-content p-mb-0">
                <span class="text__black p-text-left"> {{item[expCol.field]}} </span>
              </p>
            </ng-template>
          </q-info>
        </div>
 
      </div>
    </td>
  </tr>
</ng-template>
</p-table>
</p-card>