@things-factory/board-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.
Files changed (55) hide show
  1. package/client/apptools/favorite-tool.ts +124 -0
  2. package/client/board-list/board-tile-list.ts +272 -0
  3. package/client/board-list/group-bar-styles.ts +63 -0
  4. package/client/board-list/group-bar.ts +99 -0
  5. package/client/board-list/play-group-bar.ts +88 -0
  6. package/client/board-provider.ts +92 -0
  7. package/client/bootstrap.ts +39 -0
  8. package/client/data-grist/board-editor.ts +113 -0
  9. package/client/data-grist/board-renderer.ts +134 -0
  10. package/client/data-grist/color-map-editor.ts +17 -0
  11. package/client/data-grist/color-ranges-editor.ts +17 -0
  12. package/client/graphql/board-template.ts +141 -0
  13. package/client/graphql/board.ts +273 -0
  14. package/client/graphql/favorite-board.ts +25 -0
  15. package/client/graphql/group.ts +138 -0
  16. package/client/graphql/index.ts +6 -0
  17. package/client/graphql/my-board.ts +25 -0
  18. package/client/graphql/play-group.ts +189 -0
  19. package/client/index.ts +10 -0
  20. package/client/pages/attachment-list-page.ts +142 -0
  21. package/client/pages/board-list-page.ts +603 -0
  22. package/client/pages/board-modeller-page.ts +288 -0
  23. package/client/pages/board-player-by-name-page.ts +29 -0
  24. package/client/pages/board-player-page.ts +241 -0
  25. package/client/pages/board-template/board-template-list-page.ts +248 -0
  26. package/client/pages/board-viewer-by-name-page.ts +24 -0
  27. package/client/pages/board-viewer-page.ts +271 -0
  28. package/client/pages/font-list-page.ts +31 -0
  29. package/client/pages/play-list-page.ts +400 -0
  30. package/client/pages/printable-board-viewer-page.ts +54 -0
  31. package/client/pages/theme/theme-editors.ts +56 -0
  32. package/client/pages/theme/theme-list-page.ts +313 -0
  33. package/client/pages/things-scene-components-with-tools.import +0 -0
  34. package/client/pages/things-scene-components.import +0 -0
  35. package/client/route.ts +51 -0
  36. package/client/setting-let/board-view-setting-let.ts +68 -0
  37. package/client/themes/board-theme.css +77 -0
  38. package/client/things-scene-import.d.ts +4 -0
  39. package/client/viewparts/board-basic-info.ts +646 -0
  40. package/client/viewparts/board-info-link.ts +56 -0
  41. package/client/viewparts/board-info.ts +85 -0
  42. package/client/viewparts/board-template-builder.ts +134 -0
  43. package/client/viewparts/board-versions.ts +172 -0
  44. package/client/viewparts/group-info-basic.ts +267 -0
  45. package/client/viewparts/group-info-import.ts +132 -0
  46. package/client/viewparts/group-info.ts +87 -0
  47. package/client/viewparts/index.ts +3 -0
  48. package/client/viewparts/link-builder.ts +210 -0
  49. package/client/viewparts/play-group-info-basic.ts +268 -0
  50. package/client/viewparts/play-group-info-link.ts +46 -0
  51. package/client/viewparts/play-group-info.ts +81 -0
  52. package/dist-client/tsconfig.tsbuildinfo +1 -1
  53. package/dist-server/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +19 -19
  55. package/server/index.ts +0 -0
@@ -0,0 +1,132 @@
1
+ import '@material/web/icon/icon.js'
2
+ import '@material/web/button/elevated-button.js'
3
+ import '@operato/input/ox-input-file.js'
4
+
5
+ import { css, html, LitElement } from 'lit'
6
+ import { customElement, property, query, state } from 'lit/decorators.js'
7
+
8
+ import { i18next } from '@operato/i18n'
9
+
10
+ import { fetchGroup } from '../graphql'
11
+
12
+ @customElement('group-info-import')
13
+ export class GroupInfoImport extends LitElement {
14
+ static styles = [
15
+ css`
16
+ :host {
17
+ display: block;
18
+ color: var(--md-sys-color-on-surface);
19
+ background-color: var(--md-sys-color-surface);
20
+ height: 100%;
21
+ min-width: 360px;
22
+ overflow: auto;
23
+ position: relative;
24
+
25
+ --form-grid-gap: 2px 0;
26
+ --input-field-padding: var(--spacing-medium);
27
+ --legend-padding: var(--spacing-medium) 0 var(--spacing-small) 0;
28
+ }
29
+
30
+ [buttons] {
31
+ grid-column: span 12;
32
+ padding: var(--spacing-medium) 0;
33
+ text-align: right;
34
+
35
+ display: flex;
36
+ gap: var(--spacing-small);
37
+ align-items: center;
38
+ }
39
+
40
+ [buttons] md-elevated-button {
41
+ margin-left: auto;
42
+ }
43
+
44
+ input[type='checkbox'] + label,
45
+ input[type='radio'] + label {
46
+ text-align: left;
47
+ grid-column: span 11 / auto;
48
+
49
+ font: var(--form-sublabel-font);
50
+ color: var(--form-sublabel-color, var(--md-sys-color-secondary));
51
+ }
52
+
53
+ div[import] {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 4px;
57
+ padding: var(--spacing-large);
58
+ align-items: stretch;
59
+ }
60
+
61
+ @media screen and (max-width: 460px) {
62
+ :host {
63
+ width: 100vw;
64
+ }
65
+ }
66
+ `
67
+ ]
68
+
69
+ @property({ type: String }) groupId?: string
70
+
71
+ @state() group: any
72
+
73
+ @query('ox-input-file') fileUploader: any
74
+ @query('input[type=checkbox]') overwrite: any
75
+
76
+ render() {
77
+ var group = this.group || { name: '', description: '' }
78
+
79
+ return html`
80
+ ${this.groupId
81
+ ? html`
82
+ <div import>
83
+ <ox-input-file accept="*/*" multiple="true"></ox-input-file>
84
+
85
+ <div buttons>
86
+ <label> <input type="checkbox" name="overwrite" checked />&nbsp;overwrite </label>
87
+ <md-elevated-button @click=${this.importBoard.bind(this)}
88
+ ><md-icon slot="icon">publish</md-icon>${i18next.t('button.import-board')}</md-elevated-button
89
+ >
90
+ </div>
91
+ </div>
92
+ `
93
+ : html``}
94
+ `
95
+ }
96
+
97
+ updated(changes) {
98
+ if (changes.has('groupId')) {
99
+ this.refresh()
100
+ }
101
+ }
102
+
103
+ async refresh() {
104
+ if (!this.groupId) {
105
+ /* model이 없으므로, 기본 모델을 제공함. */
106
+ this.group = { name: '', description: '' }
107
+ } else {
108
+ var response = await fetchGroup(this.groupId)
109
+ this.group = response.group
110
+ }
111
+ }
112
+
113
+ async importBoard() {
114
+ this.dispatchEvent(
115
+ new CustomEvent('import-boards', {
116
+ bubbles: true,
117
+ composed: true,
118
+ detail: {
119
+ group: this.group,
120
+ files: this.fileUploader.files,
121
+ overwrite: this.overwrite.checked
122
+ }
123
+ })
124
+ )
125
+
126
+ this.close()
127
+ }
128
+
129
+ close() {
130
+ history.back()
131
+ }
132
+ }
@@ -0,0 +1,87 @@
1
+ import { css, html, LitElement } from 'lit'
2
+ import { customElement, property, state } from 'lit/decorators.js'
3
+ import { ifDefined } from 'lit/directives/if-defined.js'
4
+
5
+ import { ScrollbarStyles } from '@operato/styles'
6
+ import { privileged } from '@things-factory/auth-base/dist-client'
7
+
8
+ import './group-info-basic'
9
+ import './group-info-import'
10
+
11
+ @customElement('group-info')
12
+ export class GroupInfo extends LitElement {
13
+ static styles = [
14
+ ScrollbarStyles,
15
+ css`
16
+ :host {
17
+ height: 100%;
18
+ min-width: 360px;
19
+
20
+ display: flex;
21
+ flex-direction: column;
22
+ position: relative;
23
+ background-color: var(--md-sys-color-surface);
24
+ }
25
+
26
+ [tab] {
27
+ width: 100%;
28
+ display: flex;
29
+ }
30
+
31
+ [tab] > * {
32
+ flex: 1;
33
+ }
34
+
35
+ [content] {
36
+ flex: 1;
37
+ overflow-y: auto;
38
+ }
39
+
40
+ span {
41
+ padding: 5px 4px 1px;
42
+ font: var(--group-bar-textbutton);
43
+ background-color: var(--md-sys-color-secondary);
44
+ color: rgba(255, 255, 255, 0.8);
45
+ text-align: center;
46
+ text-transform: uppercase;
47
+ }
48
+
49
+ span[active] {
50
+ background-color: var(--md-sys-color-primary);
51
+ }
52
+
53
+ @media screen and (max-width: 460px) {
54
+ :host {
55
+ width: 100vw;
56
+ }
57
+ }
58
+ `
59
+ ]
60
+
61
+ @property({ type: String }) groupId?: string
62
+
63
+ @state() tab: string = 'basic'
64
+
65
+ render() {
66
+ return html`
67
+ ${privileged(
68
+ { privilege: 'mutation', category: 'board' },
69
+ html`
70
+ <div tab>
71
+ <span @click=${() => (this.tab = 'basic')} ?active=${this.tab == 'basic'}>basic</span>
72
+ <span @click=${() => (this.tab = 'import')} ?active=${this.tab == 'import'}>import</span>
73
+ </div>
74
+ `
75
+ )}
76
+ <div content>
77
+ ${this.tab == 'basic'
78
+ ? html`<group-info-basic groupId=${ifDefined(this.groupId)}></group-info-basic>`
79
+ : html`<group-info-import groupId=${ifDefined(this.groupId)}></group-info-import>`}
80
+ </div>
81
+ `
82
+ }
83
+
84
+ close() {
85
+ history.back()
86
+ }
87
+ }
@@ -0,0 +1,3 @@
1
+ export * from './board-info'
2
+ export * from './group-info'
3
+ export * from './play-group-info'
@@ -0,0 +1,210 @@
1
+ import '@material/web/icon/icon.js'
2
+ import '@operato/i18n/ox-i18n.js'
3
+ import '@operato/input/ox-input-key-values.js'
4
+
5
+ import gql from 'graphql-tag'
6
+ import Clipboard from 'clipboard'
7
+
8
+ import { css, html, LitElement, PropertyValues } from 'lit'
9
+ import { customElement, property, state } from 'lit/decorators.js'
10
+
11
+ import { client } from '@operato/graphql'
12
+ import { ScrollbarStyles } from '@operato/styles'
13
+ import { i18next } from '@operato/i18n'
14
+
15
+ import { hasPrivilege, privileged } from '@things-factory/auth-base/dist-client'
16
+
17
+ @customElement('link-builder')
18
+ export class LinkBuilder extends LitElement {
19
+ static styles = [
20
+ ScrollbarStyles,
21
+ css`
22
+ :host {
23
+ display: flex;
24
+ padding: var(--spacing-large);
25
+ color: var(--md-sys-color-primary);
26
+
27
+ /* for narrow mode */
28
+ flex-direction: column;
29
+ }
30
+
31
+ div[link] {
32
+ display: grid;
33
+ position: relative;
34
+ }
35
+
36
+ label {
37
+ font: var(--label-font);
38
+ text-transform: capitalize;
39
+ }
40
+
41
+ input,
42
+ select,
43
+ textarea,
44
+ ox-input-key-values {
45
+ border: var(--border-dim-color);
46
+ border-radius: var(--border-radius);
47
+ margin: var(--input-margin);
48
+ padding: var(--input-padding);
49
+ font: var(--input-font);
50
+ flex: 1 1 0%;
51
+ }
52
+
53
+ textarea {
54
+ min-height: 132px;
55
+ resize: none;
56
+ }
57
+
58
+ select:focus,
59
+ input:focus,
60
+ textarea:focus,
61
+ button {
62
+ outline: none;
63
+ }
64
+
65
+ button:hover {
66
+ border: var(--button-activ-border);
67
+ box-shadow: var(--button-active-box-shadow);
68
+ }
69
+
70
+ [button-in-field] {
71
+ position: absolute;
72
+ top: 64px;
73
+ right: 6px;
74
+
75
+ background-color: var(--md-sys-color-surface);
76
+ opacity: 0.8;
77
+ color: var(--md-sys-color-primary);
78
+ }
79
+
80
+ [warn] {
81
+ color: var(--status-warning-color, red);
82
+ }
83
+ `
84
+ ]
85
+
86
+ @property({ type: String, attribute: true }) command?: string
87
+ @property({ type: String, attribute: 'target-id' }) targetId?: string
88
+ @property({ type: String, attribute: 'target-name' }) targetName?: string
89
+
90
+ @state() link?: string
91
+ @state() keyvalues: any
92
+ @state() appliances: any[] = []
93
+ @state() targetAppliance: any
94
+ @state() key?: string
95
+ @state() byname: boolean = false
96
+
97
+ private clipboard?: Clipboard
98
+
99
+ render() {
100
+ return html`
101
+ <div link>
102
+ <h3>
103
+ <ox-title-with-help topic="operato-board/headless-link" msgid="label.headless-link"
104
+ >headless-link</ox-title-with-help
105
+ >
106
+ </h3>
107
+ <textarea id="link" .value=${this.link || ''} readonly></textarea>
108
+ <md-icon button-in-field clipboard-copy>content_copy</md-icon>
109
+ <label for="key">${i18next.t('label.headless-link query-key')}</label>
110
+ <input id="key" type="text" .value=${this.key || ''} @change=${e => (this.key = e.target.value)} />
111
+ <label>${i18next.t('label.headless-link query-parameters')}</label>
112
+ <ox-input-key-values @change=${e => (this.keyvalues = e.detail)}></ox-input-key-values>
113
+ <label>${i18next.t('label.headless-link target-appliance')}</label>
114
+
115
+ ${privileged(
116
+ { privilege: 'query', category: 'security', superUserGranted: true, domainOwnerGranted: true },
117
+ html`
118
+ <select
119
+ @change=${e => (this.targetAppliance = this.appliances.find(appliance => appliance.id == e.target.value))}
120
+ placeholder="choose appliance ..."
121
+ .value=${this.targetAppliance?.id}
122
+ >
123
+ <option value=""></option>
124
+ ${this.appliances.map(
125
+ appliance => html`
126
+ <option .value=${appliance.id} ?selected=${appliance.id == this.targetAppliance?.id}>
127
+ ${appliance.name}
128
+ </option>
129
+ `
130
+ )}
131
+ </select>
132
+ `,
133
+ html`
134
+ <span warn>To add an access token, you need to be a domain owner or have security query permissions.</span>
135
+ `
136
+ )}
137
+
138
+ <div>
139
+ <input
140
+ type="checkbox"
141
+ id="by-name"
142
+ @change=${(e: Event) => (this.byname = !!(e.target as HTMLInputElement)?.checked)}
143
+ />
144
+ <label for="by-name">${i18next.t('label.headless-link by-name')}</label>
145
+ </div>
146
+ </div>
147
+ `
148
+ }
149
+
150
+ firstUpdated() {
151
+ const copybuttons = this.renderRoot.querySelectorAll('[clipboard-copy]')
152
+
153
+ this.clipboard = new Clipboard(copybuttons, {
154
+ target: (trigger => trigger.parentElement.querySelector('textarea')) as any
155
+ })
156
+
157
+ this.getAppliances()
158
+ }
159
+
160
+ updated(changes: PropertyValues<this>) {
161
+ const command = (this.command || '') + (this.byname ? '-by-name' : '')
162
+ const target = encodeURIComponent((this.byname ? this.targetName : this.targetId) || '')
163
+
164
+ const baseUrl = document.querySelector('base')?.href
165
+ const fullUrl = new URL(command + '/' + target, baseUrl).href
166
+
167
+ const query = {
168
+ ...(this.key ? { [this.key]: JSON.stringify(this.keyvalues) } : this.keyvalues),
169
+ ...(this.targetAppliance ? { access_token: this.targetAppliance?.accessToken } : {})
170
+ }
171
+
172
+ const queryString = this.objectToQueryString(query)
173
+
174
+ this.link = fullUrl + (queryString ? '?' + queryString : '')
175
+ }
176
+
177
+ objectToQueryString(obj) {
178
+ return Object.keys(obj)
179
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
180
+ .join('&')
181
+ }
182
+
183
+ async getAppliances() {
184
+ this.appliances = []
185
+ this.targetAppliance = null
186
+
187
+ if (
188
+ await hasPrivilege({ privilege: 'query', category: 'security', superUserGranted: true, domainOwnerGranted: true })
189
+ ) {
190
+ var response = (
191
+ await client.query({
192
+ query: gql`
193
+ query {
194
+ appliances {
195
+ items {
196
+ id
197
+ name
198
+ description
199
+ accessToken
200
+ }
201
+ }
202
+ }
203
+ `
204
+ })
205
+ ).data
206
+
207
+ this.appliances = response.appliances.items
208
+ }
209
+ }
210
+ }
@@ -0,0 +1,268 @@
1
+ import '@material/web/icon/icon.js'
2
+ import '@material/web/textfield/filled-text-field.js'
3
+ import '@material/web/button/elevated-button.js'
4
+
5
+ import { css, html, LitElement } from 'lit'
6
+ import { customElement, property, state } from 'lit/decorators.js'
7
+
8
+ import { i18next } from '@operato/i18n'
9
+
10
+ import { fetchPlayGroup } from '../graphql'
11
+
12
+ @customElement('play-group-info-basic')
13
+ export class PlayGroupInfoBasic extends LitElement {
14
+ static styles = [
15
+ css`
16
+ :host {
17
+ display: block;
18
+ color: var(--md-sys-color-on-surface);
19
+ background-color: var(--md-sys-color-surface);
20
+ height: 100%;
21
+ min-width: 360px;
22
+ overflow: auto;
23
+ position: relative;
24
+
25
+ --form-grid-gap: 2px 0;
26
+ --input-field-padding: var(--spacing-medium);
27
+ --legend-padding: var(--spacing-medium) 0 var(--spacing-small) 0;
28
+ }
29
+
30
+ form {
31
+ display: grid;
32
+ grid-template-columns: repeat(12, 1fr);
33
+ grid-gap: var(--form-grid-gap);
34
+ grid-auto-rows: minmax(24px, auto);
35
+ padding: var(--spacing-large);
36
+ align-items: center;
37
+ }
38
+
39
+ md-filled-text-field {
40
+ grid-column: span 12;
41
+ padding: 10px 0;
42
+ }
43
+
44
+ [buttons] {
45
+ grid-column: span 12;
46
+ padding: var(--spacing-medium) 0;
47
+ text-align: right;
48
+
49
+ display: flex;
50
+ gap: var(--spacing-small);
51
+ align-items: center;
52
+ }
53
+
54
+ [buttons] * {
55
+ margin: 0 0 0 auto;
56
+ }
57
+
58
+ [danger] {
59
+ float: left;
60
+ margin: 0;
61
+ --md-elevated-button-icon-color: var(--md-sys-color-on-error);
62
+ --md-elevated-button-label-text-color: var(--md-sys-color-on-error);
63
+ --md-elevated-button-container-color: var(--md-sys-color-error);
64
+ }
65
+
66
+ fieldset {
67
+ display: contents;
68
+ }
69
+
70
+ legend {
71
+ grid-column: span 12;
72
+ padding: var(--legend-padding);
73
+ font: var(--legend-font);
74
+ color: var(--md-sys-color-primary);
75
+ text-transform: capitalize;
76
+ }
77
+
78
+ label {
79
+ grid-column: span 12;
80
+ text-transform: capitalize;
81
+ color: var(--md-sys-color-primary);
82
+ font: var(--label-font);
83
+ }
84
+
85
+ span {
86
+ grid-column: span 12;
87
+ border-bottom: var(--border-dim-color);
88
+ margin-bottom: var(--spacing-medium);
89
+ padding-bottom: var(--spacing-small);
90
+ font: var(--input-field-font);
91
+ }
92
+ span md-icon {
93
+ font-variation-settings: 'FILL' 1;
94
+
95
+ vertical-align: middle;
96
+ font-size: var(--fontsize-large);
97
+ color: var(--md-sys-color-primary);
98
+ }
99
+
100
+ input,
101
+ table,
102
+ select,
103
+ textarea,
104
+ [custom-input] {
105
+ grid-column: span 12;
106
+
107
+ border: var(--input-field-border);
108
+ border-radius: var(--input-field-border-radius);
109
+ margin-bottom: var(--spacing-medium);
110
+ padding: var(--input-field-padding);
111
+ font: var(--input-field-font);
112
+ }
113
+
114
+ input[type='checkbox'],
115
+ input[type='radio'] {
116
+ place-self: center;
117
+ margin: 0;
118
+ grid-column: 1;
119
+ }
120
+
121
+ input[type='checkbox'] + label,
122
+ input[type='radio'] + label {
123
+ text-align: left;
124
+ grid-column: span 11 / auto;
125
+
126
+ font: var(--form-sublabel-font);
127
+ color: var(--md-sys-color-secondary);
128
+ }
129
+
130
+ input:focus {
131
+ outline: none;
132
+ border: 1px solid var(--focus-background-color);
133
+ }
134
+
135
+ @media screen and (max-width: 460px) {
136
+ :host {
137
+ width: 100vw;
138
+ }
139
+ }
140
+ `
141
+ ]
142
+
143
+ @property({ type: String }) playGroupId?: string
144
+
145
+ @state() playGroup: any = { name: '', description: '' }
146
+
147
+ private link?: string
148
+
149
+ render() {
150
+ var playGroup = this.playGroup || { name: '', description: '' }
151
+
152
+ return html`
153
+ <form>
154
+ <fieldset>
155
+ <legend>playgroup information</legend>
156
+ <md-filled-text-field
157
+ type="text"
158
+ label=${String(i18next.t('label.name'))}
159
+ .value=${playGroup.name || ''}
160
+ @change=${e => (this.playGroup.name = e.target.value)}
161
+ ></md-filled-text-field>
162
+
163
+ <md-filled-text-field
164
+ type="text"
165
+ label=${String(i18next.t('label.description'))}
166
+ .value=${playGroup.description || ''}
167
+ @change=${e => (this.playGroup.description = e.target.value)}
168
+ ></md-filled-text-field>
169
+
170
+ <label>${i18next.t('label.creator')}</label>
171
+ <span>
172
+ <md-icon>person</md-icon> ${playGroup.creator && playGroup.creator.name}
173
+ <md-icon>schedule</md-icon> ${new Date(playGroup.createdAt).toLocaleString()}
174
+ </span>
175
+
176
+ <label>${i18next.t('label.updater')}</label>
177
+ <span>
178
+ <md-icon>person</md-icon> ${playGroup.updater && playGroup.updater.name}
179
+ <md-icon>schedule</md-icon> ${new Date(playGroup.updatedAt).toLocaleString()}
180
+ </span>
181
+
182
+ <div buttons>
183
+ ${this.playGroupId
184
+ ? html`
185
+ <md-elevated-button danger @click=${this.deletePlayGroup.bind(this)}
186
+ ><md-icon slot="icon">delete_outline</md-icon>${String(
187
+ i18next.t('button.delete')
188
+ )}</md-elevated-button
189
+ >
190
+ <md-elevated-button @click=${this.updateGroup.bind(this)}
191
+ ><md-icon slot="icon">save</md-icon>${String(i18next.t('button.save'))}</md-elevated-button
192
+ >
193
+ `
194
+ : html`
195
+ <md-elevated-button @click=${this.createPlayGroup.bind(this)}
196
+ ><md-icon slot="icon">create_new_folder</md-icon>${String(
197
+ i18next.t('button.create')
198
+ )}</md-elevated-button
199
+ >
200
+ `}
201
+ </div>
202
+ </fieldset>
203
+ </form>
204
+ `
205
+ }
206
+
207
+ updated(changes) {
208
+ if (changes.has('playGroupId')) {
209
+ this.refresh()
210
+ }
211
+ }
212
+
213
+ firstUpdated() {
214
+ setTimeout(() => {
215
+ ;(this.renderRoot.querySelector('[type=text]') as any).focus()
216
+ }, 100)
217
+ }
218
+
219
+ async refresh() {
220
+ if (!this.playGroupId) {
221
+ /* model이 없으므로, 기본 모델을 제공함. */
222
+ this.playGroup = { name: '', description: '' }
223
+ } else {
224
+ var response = await fetchPlayGroup(this.playGroupId)
225
+ this.playGroup = response.playGroup
226
+ }
227
+ }
228
+
229
+ async createPlayGroup() {
230
+ this.dispatchEvent(
231
+ new CustomEvent('create-play-group', {
232
+ detail: this.playGroup,
233
+ bubbles: true,
234
+ composed: true
235
+ })
236
+ )
237
+
238
+ this.close()
239
+ }
240
+
241
+ async updateGroup() {
242
+ this.dispatchEvent(
243
+ new CustomEvent('update-play-group', {
244
+ detail: this.playGroup,
245
+ bubbles: true,
246
+ composed: true
247
+ })
248
+ )
249
+
250
+ this.close()
251
+ }
252
+
253
+ async deletePlayGroup() {
254
+ this.dispatchEvent(
255
+ new CustomEvent('delete-play-group', {
256
+ detail: this.playGroup,
257
+ bubbles: true,
258
+ composed: true
259
+ })
260
+ )
261
+
262
+ this.close()
263
+ }
264
+
265
+ close() {
266
+ history.back()
267
+ }
268
+ }