saby-customizer 0.0.0-pre.7 → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/context.js ADDED
@@ -0,0 +1,43 @@
1
+ import { sabyRPC } from 'saby-customizer/lib.js'
2
+
3
+ const handlers = {}
4
+ /** @type {any} */
5
+ const origin = {
6
+ register: (name, handler) => {
7
+ handlers[name] = handler
8
+ }
9
+ }
10
+
11
+
12
+ export const context = new Proxy(origin, {
13
+ get: (target, name) => {
14
+ if (name in target) {
15
+ return target[name]
16
+ }
17
+ if (name in handlers) {
18
+ return handlers[name](target, name)
19
+ }
20
+ }
21
+ })
22
+
23
+
24
+ /** Получение сведений о текущем пользователе */
25
+ /**
26
+ *
27
+ * @param {object} target Общий объект с данными
28
+ * @param {string} name Имя получаемого параметра
29
+ * @returns {Promise<any>} Значение параметра, определяется реализацией
30
+ */
31
+ async function userInfo(target, name) {
32
+ const result = (await sabyRPC.call({
33
+ service: 'auth',
34
+ method: 'САП.ТекущийПользователь'
35
+ })).getRow()
36
+
37
+ target.userLogin = result.get('ЛогинПользователя')
38
+
39
+ return target[name]
40
+ }
41
+
42
+
43
+ context.register('userLogin', userInfo)
package/lib/database.js CHANGED
@@ -3,7 +3,10 @@ import { Dexie } from 'dexie'
3
3
  const db = new Dexie('saby-customizer-database')
4
4
 
5
5
 
6
- db.version(1).stores({ settings: '&key' })
6
+ db.version(2).stores({
7
+ settings: '&key',
8
+ gitBranchHistory: '++pk, &[id+branch], version, useDate'
9
+ })
7
10
 
8
11
 
9
12
  export { db }
package/lib/layout.js CHANGED
@@ -2,34 +2,89 @@ import { oom } from '@notml/core'
2
2
 
3
3
 
4
4
  /** Подложка в теневым DOM для размещения в ней элементов плагина, для изоляции их от основной верстки */
5
- oom.define(class SabyCustomizerLayout extends oom.extends(HTMLElement) {
5
+ class SabyCustomizerLayout extends oom.extends(HTMLElement) {
6
6
 
7
7
  static tagName = 'saby-customizer-layout'
8
8
  static attachShadow = true
9
9
 
10
- template = oom().head(oom
10
+ externalCSS = oom.link({ rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/@material/card/dist/mdc.card.min.css' }).dom
11
+
12
+ main = oom.section().dom
13
+
14
+ inner = oom.section(...this.options.inner).dom
15
+
16
+ template = () => {
17
+ // Атрибуты для игнорирования контейнера платформой wasaby
18
+ this.setAttribute('data-vdomignore', 'true')
19
+ this.setAttribute('contenteditable', 'true')
11
20
  // Предзагрузка стилей material внутри теневого DOM
12
- .link({ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Roboto&display=swap' })
13
- .link({ rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' })
14
- // https://material.io/resources/color/
15
- .style({
16
- ':host': {
17
- '--mdc-theme-primary': '#0d47a1',
18
- '--mdc-theme-secondary': '#0d47a1',
19
- '--mdc-theme-background': '#e1e1e1',
20
- '--mdc-theme-surface': '#fff',
21
- '--mdc-theme-on-primary': '#fff',
22
- '--mdc-theme-on-secondary': '#fff',
23
- '--mdc-theme-on-surface': '#000'
24
- }
25
- }))
21
+ this.style.display = 'none'
22
+ this.externalCSS.onload = () => { this.style.display = '' }
23
+
24
+ return oom()(oom.head(this.externalCSS, oom
25
+ .link({ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Roboto&display=swap' })
26
+ .link({ rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' })
27
+ // https://material.io/resources/color/
28
+ .style({
29
+ ':host': {
30
+ '--mdc-theme-primary': '#0d47a1',
31
+ '--mdc-theme-secondary': '#0d47a1',
32
+ '--mdc-theme-background': '#e1e1e1',
33
+ '--mdc-theme-surface': '#fff',
34
+ '--mdc-theme-on-primary': '#fff',
35
+ '--mdc-theme-on-secondary': '#fff',
36
+ '--mdc-theme-on-surface': '#000'
37
+ }
38
+ })), this.main, this.inner)
39
+ }
40
+
41
+ }
42
+
43
+ oom.define(SabyCustomizerLayout)
44
+
45
+
46
+ /** Элемент со всплывающими сообщениями */
47
+ class SabyCustomizerSnackbar extends oom.extends(HTMLElement) {
48
+
49
+ static tagName = 'saby-customizer-snackbar'
50
+
51
+ snackbar = oom.mwcSnackbar({ timeoutms: '4000', leading: true })
52
+
53
+ template = this.snackbar
54
+
55
+ /**
56
+ * @param {string} text Текст отображаемый в всплывашке
57
+ */
58
+ show(text) {
59
+ /** @type {import('@material/mwc-snackbar').Snackbar} */// @ts-ignore
60
+ const mwcSnackbar = this.snackbar.dom
61
+ /** @type {HTMLElement} */
62
+ const mwcSnackbarInner = mwcSnackbar.shadowRoot.querySelector('.mdc-snackbar')
63
+
64
+ this.snackbar({ labeltext: text })
65
+ mwcSnackbarInner.style.zIndex = '10000'
66
+ mwcSnackbarInner.style.justifyContent = 'flex-end'
67
+ mwcSnackbarInner.style.whiteSpace = 'pre-line'
68
+ mwcSnackbar.show()
69
+ }
70
+
71
+ }
72
+
73
+ oom.define(SabyCustomizerSnackbar)
26
74
 
27
- })
28
75
 
76
+ const snackbar = new SabyCustomizerSnackbar()
77
+ const layoutElement = new SabyCustomizerLayout({ inner: [snackbar] })
29
78
 
30
- const layoutElement = oom.sabyCustomizerLayout({ id: 'saby-customizer-layout' })
31
79
 
32
- oom(document.documentElement, layoutElement)
80
+ oom(document.documentElement, oom(layoutElement, { id: 'saby-customizer-layout' }))
81
+ layoutElement.main.onclick = () => {
82
+ layoutElement.main.querySelectorAll('mwc-menu[open]')
83
+ .forEach((/** @type {import('@material/mwc-menu').Menu} */menu) => menu.close())
84
+ }
33
85
 
34
86
 
35
- export const layout = layoutElement.dom.shadowRoot
87
+ export const layout = layoutElement.main
88
+ export {
89
+ snackbar
90
+ }
@@ -19,6 +19,25 @@ let lastDialog = null
19
19
  /** Компонент всплывающего диалога */
20
20
  export class PopupDialog extends oom.extends(HTMLElement, PopupDialogDefaults) {
21
21
 
22
+
23
+ /**
24
+ * Переключает открытый диалог с учетом уникального ключа диалога.
25
+ * Открывает с закрытием другого диалога, или скрывает если открыт тот же диалог
26
+ *
27
+ * @param {PopupDialogOptions} options Опции PopupDialog
28
+ */
29
+ static toggle(options) {
30
+ const exists = lastDialog && lastDialog.key === options.key
31
+
32
+ if (lastDialog) {
33
+ // Открытый диалог может быть только один
34
+ lastDialog.close()
35
+ }
36
+ if (!exists) {
37
+ oom(layout, new PopupDialog(options))
38
+ }
39
+ }
40
+
22
41
  static tagName = 'sc-popup-dialog'
23
42
  // static className = 'mdc-dialog'
24
43
  static style = oom.style({
@@ -27,7 +46,9 @@ export class PopupDialog extends oom.extends(HTMLElement, PopupDialogDefaults) {
27
46
  },
28
47
  'mwc-dialog': {
29
48
  '--mdc-dialog-z-index': '10000',
30
- '--mdc-dialog-max-width': '600px'
49
+ '--mdc-dialog-max-width': '700px',
50
+ '--mdc-dialog-min-width': '500px',
51
+ '--mdc-dialog-max-height': '700px'
31
52
  },
32
53
  'header': {
33
54
  margin: '-20px -24px 0px -24px',
@@ -41,30 +62,42 @@ export class PopupDialog extends oom.extends(HTMLElement, PopupDialogDefaults) {
41
62
  overflow: 'hidden'
42
63
  },
43
64
  'main': {
44
- margin: '0px -24px -20px -24px'
65
+ display: 'flex',
66
+ flexDirection: 'column',
67
+ margin: '0px -24px -20px -24px',
68
+ overflow: 'auto',
69
+ maxHeight: 'calc(var(--mdc-dialog-max-height) - 52px)'
45
70
  }
46
71
  })
47
72
 
73
+ // Фиксированный минимальный размер диалога по содержимому
74
+ contentMinWidth = 0
75
+ contentMinHeight = 0
76
+
48
77
  key = this.options.key
49
78
  content = typeof this.options.Element === 'string' ? this.options.Element : new this.options.Element()
79
+ main = oom.main(this.content)
80
+ header = oom.header(oom
81
+ .span(this.options.title)
82
+ .mwcIconButton({ icon: 'close', onclick: () => this.close() }))
50
83
 
51
84
  template = async () => {
52
85
  // Открытый диалог может быть только один
53
86
  this.id = 'sc-popup-dialog'
54
87
  /** @type {import('@material/mwc-dialog').Dialog} */// @ts-ignore
55
88
  this.dialog = oom
56
- .mwcDialog({ hideActions: true }, oom
57
- .header(oom
58
- .span(this.options.title)
59
- .mwcIconButton({ icon: 'close', onclick: () => this.close() }))
60
- .main(this.content))
89
+ .mwcDialog({ hideactions: true }, this.header, this.main)
61
90
  .dom
62
91
 
63
92
  oom(this, this.dialog)
64
93
 
65
- this.dialog.addEventListener('closed', () => this.closed())
94
+ this.dialog.addEventListener('closed', event => this.closed(event))
66
95
 
67
96
  await this.open()
97
+
98
+ this.fixMinimumSize()
99
+ this.observer = new MutationObserver(() => this.fixMinimumSize())
100
+ this.observer.observe(this, { childList: true, subtree: true })
68
101
  }
69
102
 
70
103
  /** Функция открытия диалога с асинхронной задержкой, что бы успело построиться дерево компонента */
@@ -81,31 +114,36 @@ export class PopupDialog extends oom.extends(HTMLElement, PopupDialogDefaults) {
81
114
  }
82
115
  }
83
116
 
84
- /** Обработчик завершения закрытия одиалога */
85
- closed() {
86
- this.dialog = lastDialog = null
87
- this.remove()
117
+ /**
118
+ * Обработчик завершения закрытия одиалога
119
+ *
120
+ * @param {Event} event Событие о закрытии
121
+ */
122
+ closed({ target }) {
123
+ if (target === this.dialog) {
124
+ this.dialog = lastDialog = null
125
+ this.remove()
126
+ }
127
+ }
128
+
129
+ /** Фиксирует минимальный размер диалога по содержимому, чтобы избежать дальнейших скачков размера окна */
130
+ fixMinimumSize() {
131
+ const width = Math.max(this.contentMinWidth, this.main.dom.clientWidth)
132
+ const height = Math.max(this.contentMinHeight, this.main.dom.clientHeight)
133
+
134
+ if (this.contentMinWidth !== width || this.contentMinHeight !== height) {
135
+ this.contentMinWidth = width
136
+ this.contentMinHeight = height
137
+
138
+ this.main({
139
+ style: {
140
+ minWidth: `${this.contentMinWidth}px`,
141
+ minHeight: `${this.contentMinHeight}px`
142
+ }
143
+ })
144
+ }
88
145
  }
89
146
 
90
147
  }
91
148
 
92
149
  oom.define(PopupDialog)
93
-
94
-
95
- /**
96
- * Переключает открытый диалог с учетом уникального ключа диалога.
97
- * Открывает с закрытием другого диалога, или скрывает если открыт тот же диалог
98
- *
99
- * @param {PopupDialogOptions} options Опции PopupDialog
100
- */
101
- export function toggleDialog(options) {
102
- const exists = lastDialog && lastDialog.key === options.key
103
-
104
- if (lastDialog) {
105
- // Открытый диалог может быть только один
106
- lastDialog.close()
107
- }
108
- if (!exists) {
109
- oom(layout, new PopupDialog(options))
110
- }
111
- }
@@ -1,2 +1,3 @@
1
1
  // Собрать стату по использованию фич кастомайзера
2
2
  // https://online.sbis.ru/news/4a005fd5-c99b-4e8d-a78b-eeb67b78d892
3
+ // https://git.sbis.ru/contragents/online/-/blob/rc-22.1200/client/ContractorCommon/_statisticCollection/Helper.ts
@@ -68,44 +68,86 @@ function parseRichEditorJSON(json) {
68
68
 
69
69
  /**
70
70
  * @typedef CardData Данные карточки документа
71
+ * @property {string} id Идентификатор документа
71
72
  * @property {string} number Номер документа
73
+ * @property {string} date Дата создания документа
74
+ * @property {string} name Название регламента Задача/Ошибка и т.д.
75
+ * @property {string} uuid UUID документа (для открытия ссылки)
76
+ * @property {string} url Ссылка на документ в отдельной вкладке
77
+ * @property {string} author Автор документа
78
+ * @property {string[]} milestones Список доступных вех (версий)
79
+ * @property {string} version Ближайшая версия
80
+ * @property {string} description Краткий текст описания задачи
81
+ */
82
+ /**
83
+ * @typedef Card Экземпляр карточки документа
84
+ * @property {HTMLElement} element DOM элемент карточки
85
+ * @property {CardData} data Данные карточки
72
86
  */
73
87
  /**
74
88
  * @typedef OpenCardsFilter Фильтры для выборки документов
75
89
  * @property {Array<string>} [names] Фильтр по названию документа
90
+ * @property {string} [id] Идентификатор документа
76
91
  * @property {boolean} [hasVersion] Документ включен хотя бы в 1 веху
92
+ * @property {boolean} [duplicates] Показать/скрыть из ответа документы которые открыты дважды
93
+ */
94
+ /**
95
+ * @typedef ReactWasabyControl filter
96
+ * @property {{record:{get:(name:string)=>any}}} props Опции компонента
97
+ */
98
+ /**
99
+ * @typedef WasabyControl Реализация компонентов платформы Wasaby в в составе DOM элемента
100
+ * @property {HTMLElement} element Ссылка на DOM элемент
101
+ * @property {{record:{get:(name:string)=>any}}} options Опции компонента
102
+ * @property {ReactWasabyControl} control Доп. вложенность контрола, для реализации на React
77
103
  */
78
104
  /**
79
105
  * Собирает из верстки страницы список открытых в данный момент карточек документов ЭДО
80
106
  *
81
107
  * @param {OpenCardsFilter} [filter] Параметры фильтрации документов
82
- * @returns {Array<{element:HTMLElement, data:CardData}>} Список открытых карточке документов
108
+ * @returns {Array<Card>} Список открытых карточке документов
83
109
  */
84
110
  export function getListOfOpenCards(filter = {}) {
85
111
  const cardNodes = document.querySelectorAll('.edo3-Dialog')
86
112
  const cards = []
113
+ const exists = []
87
114
 
88
115
  for (const cardNode of Array.from(cardNodes)) {
89
116
  // @ts-ignore
90
117
  const { controlNodes } = cardNode
91
118
 
92
119
  if (controlNodes) {
93
- /** @type {{element:HTMLElement,options:{record:{get:(name:string)=>any}}}} */
120
+ /** @type {WasabyControl} */
94
121
  const control = controlNodes.at()
95
122
  const { element } = control
96
- const record = control?.options?.record
123
+ const record = control.options?.record || control.control?.props?.record
97
124
  const data = record && {
125
+ id: record.get('@Документ'),
98
126
  number: record.get('Номер'),
99
127
  date: getStrDate(record.get('Дата')),
100
128
  name: record.get('РП.Документ').get('Регламент').get('Название'),
101
129
  uuid: record.get('РП.Документ').get('ИдентификаторПереписки'),
130
+ url: '',
102
131
  author: record.get('ЛицоСоздал.Название') || record.get('Сотрудник.Название'),
103
132
  milestones: [],
104
- version: 'dev'
133
+ version: `${(new Date().getFullYear() + '').slice(-2)}.0000`,
134
+ description: ''
105
135
  }
106
136
 
107
- if (data) {
108
- const milestonesRS = record.get('РП.ВехаДокумента')
137
+ if (!data) {
138
+ continue
139
+ }
140
+
141
+ if (filter.id && filter.id !== data.id) {
142
+ continue
143
+ }
144
+
145
+ if (data && (filter.duplicates || !exists.includes(data.id))) {
146
+ const milestonesRS = record.get('РП.ВехаДокумента') || []
147
+
148
+ if (!filter.duplicates) {
149
+ exists.push(data.id)
150
+ }
109
151
 
110
152
  data.name = replaceDocTypeName[data.name] || data.name
111
153
  if (filter.names) {
@@ -152,7 +194,7 @@ export function getListOfOpenCards(filter = {}) {
152
194
  data.description = 'Описание задачи не заполнено'
153
195
  }
154
196
 
155
- cards.push({ element, data })
197
+ cards.unshift({ element, data })
156
198
  }
157
199
  }
158
200
  }
@@ -0,0 +1,19 @@
1
+ /* global requirejs */
2
+ export const sabyRPC = {
3
+ call: options => new Promise((resolve, reject) => {
4
+ requirejs(['Types/source'], ({ SbisService }) => {
5
+ const service = options.service ? ('/' + options.service) : ''
6
+ const method = options.method.split('.')
7
+ const params = options.params || {}
8
+ /** @type {Promise} */
9
+ const result = new SbisService({
10
+ endpoint: {
11
+ address: service + '/service/',
12
+ contract: method[0]
13
+ }
14
+ }).call(method[1], params)
15
+
16
+ result.then(resolve).catch(reject)
17
+ })
18
+ })
19
+ }