@things-factory/menu-ui 8.0.0-beta.1 → 8.0.0-beta.2

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.
@@ -1,286 +0,0 @@
1
- import '@things-factory/form-ui'
2
- import '@operato/data-grist'
3
- import './menu-management-detail'
4
-
5
- import gql from 'graphql-tag'
6
- import { css, html } from 'lit'
7
- import { customElement, property, query } from 'lit/decorators.js'
8
-
9
- import { openPopup } from '@operato/layout'
10
- import { i18next, localize } from '@operato/i18n'
11
- import { CommonButtonStyles, ScrollbarStyles } from '@operato/styles'
12
- import { isMobileDevice } from '@operato/utils'
13
- import { client, PageView } from '@things-factory/shell/client'
14
- import { gqlBuilder } from '@things-factory/utils/src'
15
-
16
- import { OxPrompt } from '@operato/popup/ox-prompt.js'
17
- import { DataGrist, FetchOption } from '@operato/data-grist'
18
-
19
- @customElement('menu-management')
20
- class MenuManagement extends localize(i18next)(PageView) {
21
- static styles = [
22
- ScrollbarStyles,
23
- css`
24
- :host {
25
- display: flex;
26
- flex-direction: column;
27
- overflow: hidden;
28
- }
29
-
30
- search-form {
31
- overflow: visible;
32
- }
33
-
34
- ox-grist {
35
- overflow-y: auto;
36
- flex: 1;
37
- }
38
- `
39
- ]
40
-
41
- @property({ type: Array }) searchFields: any[] = []
42
- @property({ type: Object }) config: any
43
- @property({ type: Object }) data: any
44
-
45
- @query('ox-grist') dataGrist!: DataGrist
46
- @query('search-form') searchForm!: HTMLFormElement
47
-
48
- render() {
49
- return html`
50
- <search-form .fields=${this.searchFields} @submit=${e => this.dataGrist.fetch()}></search-form>
51
-
52
- <ox-grist
53
- .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
54
- .config=${this.config}
55
- .fetchHandler="${this.fetchHandler.bind(this)}"
56
- ></ox-grist>
57
- `
58
- }
59
-
60
- get context() {
61
- return {
62
- title: i18next.t('title.menu_management'),
63
- actions: [
64
- {
65
- title: i18next.t('button.save'),
66
- action: this.save.bind(this),
67
- ...CommonButtonStyles.save
68
- },
69
- {
70
- title: i18next.t('button.delete'),
71
- action: this.delete.bind(this),
72
- ...CommonButtonStyles.delete
73
- }
74
- ]
75
- }
76
- }
77
-
78
- pageUpdated(_changes, _lifecycle) {
79
- if (this.active) {
80
- this.dataGrist.fetch()
81
- }
82
- }
83
-
84
- pageInitialized() {
85
- this.searchFields = [
86
- {
87
- name: 'name',
88
- label: i18next.t('label.name'),
89
- type: 'text',
90
- props: { searchOper: 'i_like' }
91
- },
92
- {
93
- name: 'description',
94
- label: i18next.t('label.description'),
95
- type: 'text',
96
- props: { searchOper: 'i_like' }
97
- }
98
- ]
99
-
100
- this.config = {
101
- rows: { selectable: { multiple: true } },
102
- columns: [
103
- { type: 'gutter', gutterName: 'dirty' },
104
- { type: 'gutter', gutterName: 'sequence' },
105
- { type: 'gutter', gutterName: 'row-selector', multiple: true },
106
- {
107
- type: 'gutter',
108
- gutterName: 'button',
109
- icon: 'reorder',
110
- handlers: {
111
- click: (_columns, _data, _column, record, _rowIndex) => {
112
- if (record.id && record.name) this.openMenuDetail(record.id, record.name)
113
- }
114
- }
115
- },
116
- {
117
- type: 'string',
118
- name: 'name',
119
- header: i18next.t('field.name'),
120
- record: { editable: true, align: 'left' },
121
- sortable: true,
122
- width: 150
123
- },
124
- {
125
- type: 'integer',
126
- name: 'rank',
127
- header: i18next.t('field.rank'),
128
- record: { editable: true },
129
- sortable: true,
130
- width: 80
131
- },
132
- {
133
- type: 'string',
134
- name: 'description',
135
- header: i18next.t('field.description'),
136
- record: { editable: true, align: 'left' },
137
- sortable: true,
138
- width: 200
139
- },
140
- {
141
- type: 'boolean',
142
- name: 'hiddenFlag',
143
- header: i18next.t('field.hidden_flag'),
144
- record: { editable: false },
145
- sortable: true,
146
- width: 80
147
- },
148
- {
149
- type: 'datetime',
150
- name: 'updatedAt',
151
- header: i18next.t('field.updated_at'),
152
- record: { editable: false },
153
- sortable: true,
154
- width: 150
155
- },
156
- {
157
- type: 'object',
158
- name: 'updater',
159
- header: i18next.t('field.updater'),
160
- record: { editable: false },
161
- sortable: true,
162
- width: 150
163
- }
164
- ]
165
- }
166
- }
167
-
168
- async fetchHandler({ page, limit, sorters = [{ name: 'rank' }, { name: 'name' }] }: FetchOption) {
169
- const response = await client.query({
170
- query: gql`
171
- query {
172
- menus(${gqlBuilder.buildArgs({
173
- filters: [...this.searchForm.queryFilters, { name: 'menuType', operator: 'eq', value: 'MENU' }],
174
- pagination: { page, limit },
175
- sortings: sorters
176
- })}) {
177
- items {
178
- id
179
- name
180
- rank
181
- description
182
- hiddenFlag
183
- updatedAt
184
- updater{
185
- id
186
- name
187
- description
188
- }
189
- }
190
- total
191
- }
192
- }
193
- `
194
- })
195
-
196
- return {
197
- total: response.data.menus.total || 0,
198
- records: response.data.menus.items || []
199
- }
200
- }
201
-
202
- async save() {
203
- const patches = this.getPatches()
204
- if (patches && patches.length) {
205
- const response = await client.query({
206
- query: gql`
207
- mutation {
208
- updateMultipleMenu(${gqlBuilder.buildArgs({
209
- patches
210
- })}) {
211
- name
212
- }
213
- }
214
- `
215
- })
216
-
217
- if (!response.errors) this.dataGrist.fetch()
218
- } else {
219
- OxPrompt.open({
220
- title: i18next.t('text.nothing_changed'),
221
- text: i18next.t('text.there_is_nothing_to_save')
222
- })
223
- }
224
- }
225
-
226
- async delete() {
227
- const ids = this.dataGrist.selected.map(record => record.id)
228
- if (ids && ids.length > 0) {
229
- if (
230
- await OxPrompt.open({
231
- type: 'warning',
232
- title: i18next.t('button.delete'),
233
- text: i18next.t('text.are_you_sure'),
234
- confirmButton: { text: i18next.t('button.delete') },
235
- cancelButton: { text: i18next.t('button.cancel') }
236
- })
237
- ) {
238
- const response = await client.query({
239
- query: gql`
240
- mutation {
241
- deleteMenus(${gqlBuilder.buildArgs({ ids })})
242
- }
243
- `
244
- })
245
-
246
- if (!response.errors) {
247
- this.dataGrist.fetch()
248
- }
249
- }
250
- } else {
251
- OxPrompt.open({
252
- title: i18next.t('text.nothing_selected'),
253
- text: i18next.t('text.there_is_nothing_to_delete')
254
- })
255
- }
256
- }
257
-
258
- getPatches() {
259
- let patches = this.dataGrist.dirtyRecords
260
-
261
- if (patches && patches.length) {
262
- patches = patches.map(menu => {
263
- let patchField: any = menu.id ? { id: menu.id } : {}
264
- const dirtyFields = menu.__dirtyfields__
265
- for (let key in dirtyFields) {
266
- patchField[key] = dirtyFields[key].after
267
- }
268
-
269
- patchField.cuFlag = menu.__dirty__
270
- patchField.menuType = 'MENU'
271
-
272
- return patchField
273
- })
274
- }
275
-
276
- return patches
277
- }
278
-
279
- openMenuDetail(menuId, menuName) {
280
- openPopup(html` <menu-management-detail .menuId=${menuId}></menu-management-detail> `, {
281
- backdrop: true,
282
- size: 'large',
283
- title: `${i18next.t('title.menu_management_detail')} - ${menuName}`
284
- })
285
- }
286
- }
@@ -1,209 +0,0 @@
1
- import '@material/mwc-button'
2
- import '@things-factory/component-ui'
3
- import '../components/role-select-popup'
4
- import '../components/child-menus-selector'
5
-
6
- import gql from 'graphql-tag'
7
- import { css, html } from 'lit'
8
- import { customElement, property } from 'lit/decorators.js'
9
-
10
- import { openPopup } from '@operato/layout'
11
- import { i18next, localize } from '@operato/i18n'
12
- import { client, PageView } from '@things-factory/shell/client'
13
-
14
- @customElement('role-menus-management')
15
- class RoleMenusManagement extends localize(i18next)(PageView) {
16
- static styles = [
17
- css`
18
- :host {
19
- display: flex;
20
- flex-direction: column;
21
- background-color: var(--md-sys-color-background);
22
- padding: var(--spacing-large);
23
- overflow: auto;
24
- }
25
- [input-container] {
26
- display: flex;
27
- background-color: var(--md-sys-color-surface);
28
- margin: var(--spacing-large) 0;
29
- padding: var(--spacing-large);
30
- border-radius: var(--border-radius);
31
- box-shadow: var(--box-shadow);
32
- clear: both;
33
- gap: 10px;
34
- }
35
- mwc-buton {
36
- margin: auto;
37
- }
38
- h2 {
39
- margin: var(--title-margin);
40
- font: var(--title-font);
41
- color: var(--title-text-color);
42
- }
43
- [subtitle] {
44
- padding: var(--subtitle-padding);
45
- font: var(--subtitle-font);
46
- color: var(--subtitle-text-color);
47
- }
48
- input {
49
- border: var(--border-dim-color);
50
- border-radius: var(--border-radius);
51
- padding: var(--input-padding);
52
- min-width: 250px;
53
- font: var(--input-font);
54
- }
55
- label {
56
- display: flex;
57
- }
58
- @media screen and (max-width: 480px) {
59
- input {
60
- min-width: 0;
61
- }
62
- }
63
- `
64
- ]
65
-
66
- @property({ type: Object }) selectedRole: any
67
- @property({ type: Array }) parentMenus: any[] = []
68
- @property({ type: Array }) roleMenus: any
69
- @property({ type: Object }) menuSet: any
70
- @property({ type: Object }) targetRole: any
71
-
72
- get context() {
73
- return {
74
- title: i18next.t('title.role_menus_management')
75
- }
76
- }
77
-
78
- render() {
79
- const data = { menus: this.parentMenus || [] }
80
- const menuSet = this.menuSet || {}
81
-
82
- return html`
83
- <h2>${i18next.t('title.menus_by_role')}</h2>
84
- <div subtitle>${i18next.t('field.role')}</div>
85
- <div input-container>
86
- <label><input name="name" type="text" disabled /></label>
87
- <mwc-button @click=${() => this.onSelectRole()} label="${i18next.t('button.select')}" raised></mwc-button>
88
- </div>
89
-
90
- <div subtitle>${i18next.t('title.menus')}</div>
91
- <div id="children-menus">
92
- <div>
93
- <quick-find-list
94
- .data="${data}"
95
- .contentRenderer="${(item, tabKey) =>
96
- this.targetRole
97
- ? html`<child-menus-selector
98
- @updateRoleMenus="${async () => {
99
- await this.getMenus(this.targetRole)
100
- }}"
101
- .parentMenu=${item}
102
- .roleMenus=${menuSet[item.name]}
103
- .targetRole=${this.targetRole}
104
- ></child-menus-selector>`
105
- : this.showToast(i18next.t('error.value is empty', { value: i18next.t('field.role') }))}"
106
- ></quick-find-list>
107
- </div>
108
- </div>
109
- `
110
- }
111
-
112
- get roleInput() {
113
- return this.renderRoot.querySelector('input') as HTMLInputElement
114
- }
115
-
116
- async pageUpdated(changes) {
117
- if (this.active) {
118
- this.refresh()
119
- }
120
- }
121
-
122
- async refresh() {
123
- const response = await client.query({
124
- query: gql`
125
- query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
126
- menus(filters: $filters, pagination: $pagination, sortings: $sortings) {
127
- items {
128
- id
129
- name
130
- description
131
- children {
132
- id
133
- name
134
- }
135
- }
136
- total
137
- }
138
- }
139
- `,
140
- variables: {
141
- filters: [],
142
- pagination: {},
143
- sortings: [{ name: 'name' }]
144
- }
145
- })
146
-
147
- if (!response.errors?.length) {
148
- const menus = response.data.menus.items || []
149
- this.parentMenus = menus.filter(menu => menu.children?.length)
150
- }
151
- }
152
-
153
- async getMenus(targetRole) {
154
- const response = await client.query({
155
- query: gql`
156
- query ($roleId: String!) {
157
- roleMenus(roleId: $roleId) {
158
- items {
159
- id
160
- name
161
- description
162
- parent {
163
- id
164
- name
165
- }
166
- }
167
- }
168
- }
169
- `,
170
- variables: {
171
- roleId: targetRole.id
172
- }
173
- })
174
-
175
- if (!response.errors?.length) {
176
- this.roleMenus = response.data.roleMenus.items || []
177
- this.menuSet = (this.roleMenus || []).reduce((menuSet, menu) => {
178
- const parentMenu = menu.parent.name
179
- if (!menuSet[parentMenu]) menuSet[parentMenu] = []
180
- menuSet[parentMenu].push(menu)
181
-
182
- return menuSet
183
- }, {})
184
- }
185
- }
186
-
187
- onSelectRole() {
188
- openPopup(
189
- html`
190
- <role-select-popup
191
- @selected="${e => {
192
- this.selectedRole = e.detail
193
- this.roleInput.value = this.selectedRole.name
194
- this.targetRole = this.selectedRole || {}
195
- this.getMenus(this.targetRole)
196
- }}"
197
- ></role-select-popup>
198
- `,
199
- {
200
- size: 'large',
201
- title: `${i18next.t('title.role_select')}`
202
- }
203
- )
204
- }
205
-
206
- showToast(message) {
207
- document.dispatchEvent(new CustomEvent('notify', { detail: { message, option: { timer: 1000 } } }))
208
- }
209
- }
package/client/route.ts DELETED
@@ -1,15 +0,0 @@
1
- export default function route(page) {
2
- switch (page) {
3
- case 'menu-list':
4
- import('./pages/menu-list-page')
5
- return page
6
-
7
- case 'menus':
8
- import('./pages/menu-management')
9
- return page
10
-
11
- case 'role-menus':
12
- import('./pages/role-menus-management')
13
- return page
14
- }
15
- }
@@ -1,31 +0,0 @@
1
- body {
2
- --menu-bar-background-color: var(--md-sys-color-primary);
3
- --menu-bar-textbutton: 16px/32px var(--theme-font);
4
- --menu-bar-textbutton-active: bold var(--menu-bar-textbutton);
5
- --menu-bar-active-line-color: var(--focus-color);
6
- --menu-bar-line: solid 3px rgba(0, 0, 0, 0.2);
7
-
8
- --menu-tools-background-color: var(--md-sys-color-secondary);
9
- --menu-tools-iconbutton-size: var(--icon-tiny-size);
10
- --menu-tools-color: #87cfd0;
11
- --menu-tools-active-color: #fff;
12
-
13
- --menu-tree-toplevel-color: #d0fdee;
14
- --menu-tree-toplevel-font: normal 15px var(--theme-font);
15
- --menu-tree-toplevel-icon-size: 5px;
16
- --menu-tree-toplevel-icon-border: 2px solid var(--menu-tree-toplevel-color);
17
- --menu-tree-toplevel-border-bottom: 1px solid rgba(0, 0, 0, 0.15);
18
- --menu-tree-grouplevel-font: normal 13px var(--theme-font);
19
- --menu-tree-grouplevel-color: #d0d0d0;
20
- --menu-tree-grouplevel-background-color: rgba(0, 0, 0, 0.3);
21
- --menu-tree-grouplevel-border-bottom: 1px solid rgba(0, 0, 0, 0.5);
22
- --menu-tree-grouplevel-active-border-left: 3px solid var(--md-sys-color-primary);
23
- --menu-tree-focus-color: #fff;
24
- --menu-tree-favorite-color: var(--menu-tree-toplevel-color);
25
-
26
- --menu-domain-background-color: var(--md-sys-color-secondary);
27
- --menu-domain-font: bold 14px var(--theme-font);
28
- --menu-domain-color: #fff;
29
- --menu-domain-icon-color: #d0fdee;
30
- --menu-domain-padding: 0px 7px 4px 8px;
31
- }
@@ -1,110 +0,0 @@
1
- import '@material/web/icon/icon.js'
2
-
3
- import { css, html, LitElement } from 'lit'
4
- import { customElement, property } from 'lit/decorators.js'
5
-
6
- import ScrollBooster from 'scrollbooster'
7
-
8
- @customElement('menu-bar')
9
- export default class MenuBar extends LitElement {
10
- static styles = [
11
- css`
12
- :host {
13
- background-color: var(--menu-bar-background-color, #242d30);
14
-
15
- overflow-x: hidden;
16
- }
17
-
18
- ul {
19
- display: flex;
20
- list-style: none;
21
- margin: 0;
22
- padding: 0;
23
- white-space: nowrap;
24
- }
25
-
26
- li {
27
- display: inline-block;
28
- padding: 0px 3px;
29
-
30
- border-bottom: var(--menu-bar-line);
31
- }
32
-
33
- li[active] {
34
- border-color: var(--menu-bar-active-line-color, red);
35
- }
36
-
37
- li a {
38
- display: block;
39
- padding: 5px 4px 1px 4px;
40
- text-decoration: none;
41
- font: var(--menu-bar-textbutton);
42
- color: rgba(255, 255, 255, 0.8);
43
- }
44
-
45
- li[active] a {
46
- font: var(--menu-bar-textbutton-active);
47
- color: rgba(255, 255, 255, 1);
48
- }
49
- `
50
- ]
51
-
52
- @property({ type: Array }) menus: any[] = []
53
- @property({ type: String }) menuId?: string
54
-
55
- private __sb: any
56
-
57
- render() {
58
- var topmenus = this.menus || []
59
-
60
- return html`
61
- <ul>
62
- <li ?active=${!this.menuId}>
63
- <a href="menu-list"><md-icon>star</md-icon></a>
64
- </li>
65
-
66
- ${topmenus.map(
67
- (menu, idx) => html`
68
- <li ?active=${this.menuId === String(idx)}>
69
- <a href=${`${menu.routing || 'menu-list'}/${idx}`}>${menu.name}</a>
70
- </li>
71
- `
72
- )}
73
- </ul>
74
- `
75
- }
76
-
77
- _onWheelEvent(e) {
78
- var delta = Math.max(-1, Math.min(1, e.wheelDelta || -e.detail))
79
- this.scrollLeft -= delta * 40
80
-
81
- e.preventDefault()
82
- }
83
-
84
- updated(change) {
85
- if (change.has('menus')) {
86
- /* menus가 바뀔 때마다, contents의 폭이 달라지므로, 다시 폭을 계산해준다. */
87
- this.__sb && this.__sb.updateMetrics()
88
- }
89
-
90
- if (change.has('menuId')) {
91
- var active = this.renderRoot.querySelector('li[active]')
92
- active && active.scrollIntoView()
93
- }
94
- }
95
-
96
- firstUpdated() {
97
- var scrollTarget = this.renderRoot.querySelector('ul')
98
-
99
- scrollTarget?.addEventListener('mousewheel', this._onWheelEvent.bind(this), false)
100
-
101
- this.__sb = new ScrollBooster({
102
- viewport: this,
103
- content: scrollTarget,
104
- mode: 'x',
105
- onUpdate: data => {
106
- this.scrollLeft = data.position.x
107
- }
108
- })
109
- }
110
- }