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/features/doc-copy-link.js +17 -0
- package/features/git-branch.js +770 -40
- package/lib/context.js +43 -0
- package/lib/database.js +4 -1
- package/lib/layout.js +75 -20
- package/lib/popup-dialog.js +69 -31
- package/{saby-lib → lib/saby-lib}/cloud-statistic.js +1 -0
- package/{saby-lib → lib/saby-lib}/edo.js +49 -7
- package/lib/saby-lib/rpc.js +19 -0
- package/lib/saby-lib/toolbar.js +272 -0
- package/lib.js +8 -0
- package/material.js +9161 -5144
- package/octicons.js +4673 -0
- package/package.json +25 -17
- package/{lib → third-party}/material.js +5 -0
- package/third-party/octicons.js +3 -0
- package/userscript.js +9 -4
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(
|
|
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
|
-
|
|
5
|
+
class SabyCustomizerLayout extends oom.extends(HTMLElement) {
|
|
6
6
|
|
|
7
7
|
static tagName = 'saby-customizer-layout'
|
|
8
8
|
static attachShadow = true
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
.
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
'
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
87
|
+
export const layout = layoutElement.main
|
|
88
|
+
export {
|
|
89
|
+
snackbar
|
|
90
|
+
}
|
package/lib/popup-dialog.js
CHANGED
|
@@ -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': '
|
|
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
|
-
|
|
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({
|
|
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',
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
}
|
|
@@ -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<
|
|
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 {
|
|
120
|
+
/** @type {WasabyControl} */
|
|
94
121
|
const control = controlNodes.at()
|
|
95
122
|
const { element } = control
|
|
96
|
-
const record = control
|
|
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: '
|
|
133
|
+
version: `${(new Date().getFullYear() + '').slice(-2)}.0000`,
|
|
134
|
+
description: ''
|
|
105
135
|
}
|
|
106
136
|
|
|
107
|
-
if (data) {
|
|
108
|
-
|
|
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.
|
|
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
|
+
}
|