@things-factory/setting-ui 8.0.0-beta.9 → 8.0.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.
@@ -0,0 +1,299 @@
1
+ import '@operato/data-grist/ox-grist.js'
2
+ import '@operato/data-grist/ox-filters-form.js'
3
+ import '@operato/data-grist/ox-sorters-control.js'
4
+ import '@operato/data-grist/ox-record-creator.js'
5
+
6
+ import { getEditor, getRenderer } from '@operato/data-grist'
7
+ import { i18next, localize } from '@operato/i18n'
8
+ import { PageView, store } from '@operato/shell'
9
+ import { client, gqlContext } from '@operato/graphql'
10
+ import { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
11
+ import { OxPrompt } from '@operato/popup/ox-prompt.js'
12
+ import { isMobileDevice } from '@operato/utils'
13
+
14
+ import gql from 'graphql-tag'
15
+ import { css, html } from 'lit'
16
+ import { customElement, property, query, state } from 'lit/decorators.js'
17
+ import { connect } from 'pwa-helpers/connect-mixin'
18
+
19
+ @customElement('setting-list')
20
+ export class SettingList extends connect(store)(localize(i18next)(PageView)) {
21
+ static styles = [
22
+ CommonGristStyles,
23
+ ScrollbarStyles,
24
+ CommonHeaderStyles,
25
+ css`
26
+ :host {
27
+ display: flex;
28
+ flex-direction: column;
29
+ overflow: hidden;
30
+ }
31
+
32
+ ox-grist {
33
+ overflow-y: auto;
34
+ flex: 1;
35
+ }
36
+
37
+ ox-filters-form {
38
+ flex: 1;
39
+ }
40
+ `
41
+ ]
42
+
43
+ @state() private config: any
44
+ @state() private data: any
45
+ @state() private mode: string = isMobileDevice() ? 'LIST' : 'GRID'
46
+ @state() private refreshHandlers: any[] = []
47
+
48
+ @query('ox-grist') private dataGrist: any
49
+
50
+ render() {
51
+ return html`
52
+ <ox-grist .mode=${this.mode} auto-fetch .config=${this.config} .fetchHandler=${this.fetchHandler.bind(this)}>
53
+ <div slot="headroom" class="header">
54
+ <div class="filters">
55
+ <ox-filters-form></ox-filters-form>
56
+ </div>
57
+ </div>
58
+ </ox-grist>
59
+ `
60
+ }
61
+
62
+ get context() {
63
+ return {
64
+ title: i18next.t('title.setting'),
65
+ help: 'setting/settings',
66
+ actions: [
67
+ {
68
+ title: i18next.t('button.save'),
69
+ action: this._saveSettings.bind(this),
70
+ ...CommonButtonStyles.save
71
+ },
72
+ {
73
+ title: i18next.t('button.delete'),
74
+ action: this._deleteSettings.bind(this),
75
+ ...CommonButtonStyles.delete
76
+ }
77
+ ],
78
+ exportable: {
79
+ name: i18next.t('title.setting'),
80
+ data: this._exportableData.bind(this)
81
+ },
82
+ importable: {
83
+ handler: () => {}
84
+ }
85
+ }
86
+ }
87
+
88
+ pageUpdated(changes, lifecycle) {
89
+ if (this.active) {
90
+ this.dataGrist.fetch()
91
+
92
+ this.config = {
93
+ list: {
94
+ fields: ['name', 'description', 'value']
95
+ },
96
+ rows: { selectable: { multiple: true } },
97
+ columns: [
98
+ { type: 'gutter', gutterName: 'dirty' },
99
+ { type: 'gutter', gutterName: 'sequence' },
100
+ { type: 'gutter', gutterName: 'row-selector', multiple: true },
101
+ {
102
+ type: 'string',
103
+ name: 'name',
104
+ header: i18next.t('field.name'),
105
+ record: { editable: true, align: 'left' },
106
+ sortable: true,
107
+ filter: 'search',
108
+ width: 235
109
+ },
110
+ {
111
+ type: 'string',
112
+ name: 'description',
113
+ header: i18next.t('field.description'),
114
+ record: { editable: true, align: 'left' },
115
+ sortable: true,
116
+ filter: 'search',
117
+ width: 275
118
+ },
119
+ {
120
+ type: 'code',
121
+ name: 'category',
122
+ header: i18next.t('field.category'),
123
+ record: { editable: true, codeName: 'SETTING_CATEGORIES' },
124
+ sortable: true,
125
+ filter: 'search',
126
+ width: 100
127
+ },
128
+ {
129
+ type: 'string',
130
+ name: 'value',
131
+ header: i18next.t('field.value'),
132
+ record: {
133
+ editor: function (value, column, record, rowIndex, field) {
134
+ return getEditor(record.category)(value, column, record, rowIndex, field)
135
+ },
136
+ renderer: function (value, column, record, rowIndex, field) {
137
+ return getRenderer(record.category)(value, column, record, rowIndex, field)
138
+ },
139
+ editable: true
140
+ },
141
+ sortable: true,
142
+ width: 180
143
+ },
144
+ {
145
+ type: 'object',
146
+ name: 'updater',
147
+ header: i18next.t('field.updater'),
148
+ record: { editable: false, align: 'left' },
149
+ sortable: true,
150
+ width: 90
151
+ },
152
+ {
153
+ type: 'datetime',
154
+ name: 'updatedAt',
155
+ header: i18next.t('field.updated_at'),
156
+ record: { editable: false, align: 'left' },
157
+ sortable: true,
158
+ width: 180
159
+ }
160
+ ]
161
+ }
162
+ }
163
+ }
164
+
165
+ async fetchHandler({ filters, page, limit, sorters = [] }) {
166
+ const pagination = { page, limit }
167
+ const sortings = sorters
168
+
169
+ const response = await client.query({
170
+ query: gql`
171
+ query settings($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
172
+ settings(filters: $filters, pagination: $pagination, sortings: $sortings) {
173
+ items {
174
+ id
175
+ name
176
+ description
177
+ category
178
+ value
179
+ updatedAt
180
+ updater {
181
+ id
182
+ name
183
+ description
184
+ }
185
+ }
186
+ total
187
+ }
188
+ }
189
+ `,
190
+ variables: { filters, pagination, sortings },
191
+ context: gqlContext()
192
+ })
193
+
194
+ return {
195
+ total: response.data.settings.total || 0,
196
+ records: response.data.settings.items || []
197
+ }
198
+ }
199
+
200
+ async _saveSettings() {
201
+ let patches = this.dataGrist.dirtyRecords
202
+
203
+ if (!patches?.length) {
204
+ return this.showToast(i18next.t('text.nothing_changed'))
205
+ }
206
+
207
+ patches = patches.map(setting => {
208
+ let patchField: any = setting.id ? { id: setting.id } : {}
209
+ const dirtyFields = setting.__dirtyfields__
210
+ for (let key in dirtyFields) {
211
+ patchField[key] = dirtyFields[key].after
212
+ }
213
+ patchField.cuFlag = setting.__dirty__
214
+
215
+ return patchField
216
+ })
217
+
218
+ let checkValidation = true
219
+ patches.forEach(patch => {
220
+ if (patch.cuFlag === '+' && (!patch.name || !patch.category)) return (checkValidation = false)
221
+ })
222
+
223
+ if (!checkValidation) return this.showToast(i18next.t('error.value is empty', { value: 'name or category' }))
224
+
225
+ const response = await client.mutate({
226
+ mutation: gql`
227
+ mutation updateMultipleSetting($patches: [SettingPatch!]!) {
228
+ updateMultipleSetting(patches: $patches) {
229
+ name
230
+ }
231
+ }
232
+ `,
233
+ variables: { patches },
234
+ context: gqlContext()
235
+ })
236
+
237
+ if (!response.errors) {
238
+ this.showToast(i18next.t('text.data_updated_successfully'))
239
+ this.dataGrist.fetch()
240
+ this.applyRefreshHandlers()
241
+ }
242
+ }
243
+
244
+ async _deleteSettings() {
245
+ if (!this.dataGrist.selected?.length) {
246
+ return this.showToast(i18next.t('text.there_is_nothing_to_delete'))
247
+ }
248
+
249
+ const names = this.dataGrist.selected.map(record => record.name)
250
+
251
+ if (
252
+ await OxPrompt.open({
253
+ type: 'warning',
254
+ title: i18next.t('button.delete'),
255
+ text: i18next.t('text.are_you_sure'),
256
+ confirmButton: { text: i18next.t('button.delete') },
257
+ cancelButton: { text: i18next.t('button.cancel') }
258
+ })
259
+ ) {
260
+ const response = await client.mutate({
261
+ mutation: gql`
262
+ mutation deleteSettings($names: [String!]!) {
263
+ deleteSettings(names: $names)
264
+ }
265
+ `,
266
+ variables: { names },
267
+ context: gqlContext()
268
+ })
269
+
270
+ if (!response.errors) {
271
+ await OxPrompt.open({
272
+ type: 'success',
273
+ title: i18next.t('text.completed'),
274
+ text: i18next.t('text.data_deleted_successfully'),
275
+ confirmButton: { text: i18next.t('button.confirm') }
276
+ })
277
+
278
+ this.dataGrist.fetch()
279
+ this.applyRefreshHandlers()
280
+ }
281
+ }
282
+ }
283
+
284
+ applyRefreshHandlers() {
285
+ this.refreshHandlers.forEach(refreshHandler => refreshHandler())
286
+ }
287
+
288
+ _exportableData() {
289
+ return this.dataGrist.exportRecords()
290
+ }
291
+
292
+ stateChanged(state) {
293
+ this.refreshHandlers = state.setting?.refreshHandlers || []
294
+ }
295
+
296
+ showToast(message) {
297
+ document.dispatchEvent(new CustomEvent('notify', { detail: { message, option: { timer: 1000 } } }))
298
+ }
299
+ }
@@ -0,0 +1,47 @@
1
+ import { html, css } from 'lit'
2
+ import { customElement, property, query, state } from 'lit/decorators.js'
3
+ import { connect } from 'pwa-helpers/connect-mixin.js'
4
+
5
+ import { store, PageView } from '@operato/shell'
6
+ import { i18next, localize } from '@operato/i18n'
7
+
8
+ @customElement('setting-page')
9
+ export class SettingPage extends connect(store)(localize(i18next)(PageView)) {
10
+ static styles = [
11
+ css`
12
+ :host {
13
+ overflow-y: auto;
14
+ background-color: var(--md-sys-color-background);
15
+ }
16
+ div {
17
+ margin: var(--setting-icon-margin);
18
+ height: var(--setting-icon-height);
19
+ background: url(/assets/images/icon-setting.png) center top no-repeat;
20
+ background-size: contain;
21
+ }
22
+ `
23
+ ]
24
+
25
+ @state() private _settings: any[] = []
26
+
27
+ render() {
28
+ var _sortedSettings = this._settings.sort((a, b) => {
29
+ return a.seq - b.seq
30
+ })
31
+
32
+ return html`
33
+ <div class="page-icon"></div>
34
+ ${_sortedSettings.map(setting => html` ${setting.template} `)}
35
+ `
36
+ }
37
+
38
+ stateChanged(state) {
39
+ this._settings = state.setting.settings
40
+ }
41
+
42
+ get context() {
43
+ return {
44
+ title: i18next.t('title.setting')
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,15 @@
1
+ export default function route(page) {
2
+ switch (page) {
3
+ case 'setting':
4
+ import('./pages/setting')
5
+ return page
6
+
7
+ case 'settings':
8
+ import('./pages/setting-list')
9
+ return page
10
+
11
+ case 'partner_settings':
12
+ import('./pages/partner-setting-list')
13
+ return page
14
+ }
15
+ }
@@ -0,0 +1,50 @@
1
+ import { clientSettingStore } from '@operato/shell'
2
+
3
+ var prefersColorSchemeMedia
4
+
5
+ function getPrefersColorSchemeMedia() {
6
+ if (!prefersColorSchemeMedia) {
7
+ prefersColorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)')
8
+ }
9
+ return prefersColorSchemeMedia
10
+ }
11
+
12
+ async function onPreferColorSchemeChanged() {
13
+ const themeMode = ((await clientSettingStore.get('theme'))?.value || {}).mode || 'light'
14
+
15
+ if (themeMode !== 'auto') {
16
+ getPrefersColorSchemeMedia().removeEventListener('change', onPreferColorSchemeChanged)
17
+
18
+ return
19
+ }
20
+
21
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
22
+ document.body.classList.add('dark')
23
+ document.body.classList.remove('light')
24
+ } else {
25
+ document.body.classList.add('light')
26
+ document.body.classList.remove('dark')
27
+ }
28
+ }
29
+
30
+ export function setThemeMode(mode) {
31
+ getPrefersColorSchemeMedia().removeEventListener('change', onPreferColorSchemeChanged)
32
+
33
+ if (mode === 'dark') {
34
+ document.body.classList.add('dark')
35
+ document.body.classList.remove('light')
36
+ } else if (mode === 'light') {
37
+ document.body.classList.add('light')
38
+ document.body.classList.remove('dark')
39
+ } else {
40
+ if (getPrefersColorSchemeMedia().matches) {
41
+ document.body.classList.add('dark')
42
+ document.body.classList.remove('light')
43
+ } else {
44
+ document.body.classList.add('light')
45
+ document.body.classList.remove('dark')
46
+ }
47
+
48
+ getPrefersColorSchemeMedia().addEventListener('change', onPreferColorSchemeChanged)
49
+ }
50
+ }
@@ -0,0 +1,49 @@
1
+ import '@things-factory/setting-base'
2
+ import '@operato/i18n/ox-i18n.js'
3
+
4
+ import { store } from '@operato/shell'
5
+
6
+ import { html, css, LitElement } from 'lit'
7
+ import { customElement, property, query, queryAll, state } from 'lit/decorators.js'
8
+ import { connect } from 'pwa-helpers/connect-mixin'
9
+
10
+ @customElement('domain-switch-let')
11
+ export class DomainSwitchLet extends connect(store)(LitElement) {
12
+ static styles = [
13
+ css`
14
+ select {
15
+ border: var(--input-field-border);
16
+ padding: var(--input-padding);
17
+ border-radius: var(--border-radius);
18
+ font: var(--input-font);
19
+ }
20
+ `
21
+ ]
22
+
23
+ @state() domains: any[] = []
24
+ @state() domain: any
25
+
26
+ render() {
27
+ return html`
28
+ <setting-let>
29
+ <ox-i18n slot="title" msgid="title.switch domain"></ox-i18n>
30
+
31
+ <select slot="content" @change=${e => (window.location.pathname = '/auth/checkin/' + e.target.value)}>
32
+ <option value="">&nbsp</option>
33
+ ${this.domains.map(
34
+ option => html`
35
+ <option value=${option.subdomain} ?selected=${this.domain?.subdomain == option.subdomain}>
36
+ ${option.name}
37
+ </option>
38
+ `
39
+ )}
40
+ </select>
41
+ </setting-let>
42
+ `
43
+ }
44
+
45
+ stateChanged(state) {
46
+ this.domains = state.app.domains
47
+ this.domain = state.app.domain
48
+ }
49
+ }
@@ -0,0 +1,208 @@
1
+ import '@material/web/textfield/filled-text-field.js'
2
+ import '@things-factory/setting-base'
3
+ import '@operato/i18n/ox-i18n.js'
4
+
5
+ import gql from 'graphql-tag'
6
+ import { html, css, LitElement, PropertyValueMap } from 'lit'
7
+ import { customElement, property, query, queryAll, state } from 'lit/decorators.js'
8
+
9
+ import { i18next } from '@operato/i18n'
10
+ import { client } from '@operato/graphql'
11
+
12
+ @customElement('secure-iplist-setting-let')
13
+ export class SecureIPListSettingLet extends LitElement {
14
+ static styles = [
15
+ css`
16
+ div[slot='content'] {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 6px;
20
+ }
21
+
22
+ ul {
23
+ flex: 1;
24
+ background-color: var(--md-sys-color-surface);
25
+ overflow: auto;
26
+ display: grid;
27
+ grid-template-columns: 1fr 1fr;
28
+ margin: 0;
29
+ padding: var(--spacing-medium);
30
+ list-style: none;
31
+ border: 1px dashed var(--md-sys-color-outline);
32
+ border-width: 1px 0;
33
+ }
34
+
35
+ [hidden] {
36
+ display: none;
37
+ }
38
+
39
+ @media screen and (max-width: 480px) {
40
+ ul {
41
+ grid-template-columns: 1fr;
42
+ }
43
+ }
44
+ `
45
+ ]
46
+
47
+ @property({ type: Object }) value?: {
48
+ blacklist?: string[]
49
+ whitelist?: string[]
50
+ protectedlist?: string[]
51
+ privileges?: any[]
52
+ }
53
+
54
+ @state() private allPrivileges: {
55
+ privilege: string
56
+ category: string
57
+ description: string
58
+ }[] = []
59
+
60
+ @query('[name="whitelist"]') whitelist
61
+ @query('[name="blacklist"]') blacklist
62
+ @query('[name="protectedlist"]') protectedlist
63
+ @queryAll('ul[privileges] input[type=checkbox]:checked') protectedPrivileges
64
+
65
+ render() {
66
+ const { whitelist = [], blacklist = [], protectedlist = [], privileges = [] } = this.value || {}
67
+
68
+ return html`
69
+ <setting-let>
70
+ <ox-title-with-help slot="title" topic="setting/secure-ip-list" msgid="title.secure-ip-list"
71
+ >secure IP list</ox-title-with-help
72
+ >
73
+
74
+ <div slot="content" @change=${() => this.onSave()}>
75
+ <md-filled-text-field
76
+ type="text"
77
+ name="whitelist"
78
+ .label=${String(i18next.t('label.whitelist'))}
79
+ icon="health_and_safety"
80
+ value=${whitelist.join(', ')}
81
+ supporting-text=${String(i18next.t('text.supporting text for whitelist'))}
82
+ hidden
83
+ >
84
+ <md-icon slot="leading-icon">health_and_safety</md-icon>
85
+ </md-filled-text-field>
86
+
87
+ <md-filled-text-field
88
+ type="text"
89
+ name="blacklist"
90
+ .label=${String(i18next.t('label.blacklist'))}
91
+ value=${blacklist.join(', ')}
92
+ supporting-text=${String(i18next.t('text.supporting text for blacklist'))}
93
+ >
94
+ <md-icon slot="leading-icon">block</md-icon>
95
+ </md-filled-text-field>
96
+
97
+ <md-filled-text-field
98
+ type="text"
99
+ name="protectedlist"
100
+ .label=${String(i18next.t('label.protectedlist'))}
101
+ value=${protectedlist.join(', ')}
102
+ supporting-text=${String(i18next.t('text.supporting text for protected ip-list'))}
103
+ >
104
+ <md-icon slot="leading-icon">security</md-icon>
105
+ </md-filled-text-field>
106
+
107
+ <div>
108
+ <h3><ox-i18n msgid="title.protected-privileges"></ox-i18n></h3>
109
+ <div>
110
+ <input id="checkAll" type="checkbox" @change=${e => this.oncheckAll(e)} />
111
+ <label for="checkAll">Check all</label>
112
+ </div>
113
+
114
+ <ul privileges>
115
+ ${this.allPrivileges.map(privilege => {
116
+ const id = `${privilege.category}-${privilege.privilege}`
117
+ return html`
118
+ <li>
119
+ <input
120
+ type="checkbox"
121
+ id=${id}
122
+ .checked=${!!privileges.find(
123
+ p => p.privilege == privilege.privilege && p.category == privilege.category
124
+ )}
125
+ .data-privilege=${privilege}
126
+ />
127
+ <label for="${id}">${privilege.description}</label>
128
+ </li>
129
+ `
130
+ })}
131
+ </ul>
132
+ </div>
133
+ </div>
134
+ </setting-let>
135
+ `
136
+ }
137
+
138
+ async firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): Promise<void> {
139
+ var { data } = await client.query({
140
+ query: gql`
141
+ query {
142
+ privileges {
143
+ items {
144
+ privilege
145
+ category
146
+ description
147
+ }
148
+ total
149
+ }
150
+ }
151
+ `
152
+ })
153
+
154
+ this.allPrivileges = data?.privileges.items || []
155
+
156
+ var { data } = await client.query({
157
+ query: gql`
158
+ query {
159
+ secureIPList
160
+ }
161
+ `
162
+ })
163
+
164
+ this.value = data ? data.secureIPList : {}
165
+ }
166
+
167
+ checkAll(checked) {
168
+ Array.from(this.renderRoot.querySelectorAll('ul[privileges] input[type=checkbox]')).forEach(
169
+ checkbox => ((checkbox as HTMLInputElement).checked = checked)
170
+ )
171
+ }
172
+
173
+ oncheckAll(e) {
174
+ this.checkAll(e.target.checked)
175
+ }
176
+
177
+ async onSave() {
178
+ const whitelist = !this.whitelist.value ? [] : this.whitelist.value.split(',').map(ip => ip.trim())
179
+ const blacklist = !this.blacklist.value ? [] : this.blacklist.value.split(',').map(ip => ip.trim())
180
+ const protectedlist = !this.protectedlist.value ? [] : this.protectedlist.value.split(',').map(ip => ip.trim())
181
+ const privileges = Array.from(this.protectedPrivileges)
182
+ .map((e: any) => e['data-privilege'])
183
+ .map((priv: any) => {
184
+ const { description, ...others } = priv
185
+ return others
186
+ })
187
+
188
+ const iplist = {
189
+ whitelist,
190
+ blacklist,
191
+ protectedlist,
192
+ privileges
193
+ }
194
+
195
+ const { data } = await client.query({
196
+ query: gql`
197
+ mutation ($iplist: Object!) {
198
+ updateSecureIPList(iplist: $iplist)
199
+ }
200
+ `,
201
+ variables: {
202
+ iplist
203
+ }
204
+ })
205
+
206
+ this.value = data ? data.updateSecureIPList : {}
207
+ }
208
+ }