@things-factory/board-ui 10.1.40 → 10.1.42

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 (81) hide show
  1. package/dist-client/board-list/board-thumb.d.ts +33 -0
  2. package/dist-client/board-list/board-thumb.js +119 -0
  3. package/dist-client/board-list/board-thumb.js.map +1 -0
  4. package/dist-client/graphql/favorite-board.js +3 -0
  5. package/dist-client/graphql/favorite-board.js.map +1 -1
  6. package/dist-client/graphql/my-board.js +3 -0
  7. package/dist-client/graphql/my-board.js.map +1 -1
  8. package/dist-client/graphql/play-group.js +2 -0
  9. package/dist-client/graphql/play-group.js.map +1 -1
  10. package/dist-client/index.d.ts +1 -0
  11. package/dist-client/index.js +1 -0
  12. package/dist-client/index.js.map +1 -1
  13. package/dist-client/pages/board-list-page.d.ts +39 -42
  14. package/dist-client/pages/board-list-page.js +388 -284
  15. package/dist-client/pages/board-list-page.js.map +1 -1
  16. package/dist-client/pages/board-modeller-page.d.ts +2 -0
  17. package/dist-client/pages/board-modeller-page.js +18 -1
  18. package/dist-client/pages/board-modeller-page.js.map +1 -1
  19. package/dist-client/pages/board-template/board-template-list-page.d.ts +21 -10
  20. package/dist-client/pages/board-template/board-template-list-page.js +147 -136
  21. package/dist-client/pages/board-template/board-template-list-page.js.map +1 -1
  22. package/dist-client/pages/play-list-page.d.ts +22 -12
  23. package/dist-client/pages/play-list-page.js +264 -113
  24. package/dist-client/pages/play-list-page.js.map +1 -1
  25. package/dist-client/route.d.ts +1 -1
  26. package/dist-client/setting-let/board-modeller-setting-let.d.ts +21 -0
  27. package/dist-client/setting-let/board-modeller-setting-let.js +144 -0
  28. package/dist-client/setting-let/board-modeller-setting-let.js.map +1 -0
  29. package/dist-client/styles/board-card-styles.d.ts +11 -0
  30. package/dist-client/styles/board-card-styles.js +90 -0
  31. package/dist-client/styles/board-card-styles.js.map +1 -0
  32. package/dist-client/themes/board-theme.css +32 -19
  33. package/dist-client/tsconfig.tsbuildinfo +1 -1
  34. package/dist-client/utils/board-meta.d.ts +10 -0
  35. package/dist-client/utils/board-meta.js +34 -0
  36. package/dist-client/utils/board-meta.js.map +1 -0
  37. package/dist-client/viewparts/board-basic-info.d.ts +38 -7
  38. package/dist-client/viewparts/board-basic-info.js +378 -349
  39. package/dist-client/viewparts/board-basic-info.js.map +1 -1
  40. package/dist-client/viewparts/board-info-link.d.ts +5 -1
  41. package/dist-client/viewparts/board-info-link.js +79 -21
  42. package/dist-client/viewparts/board-info-link.js.map +1 -1
  43. package/dist-client/viewparts/board-info-template.d.ts +15 -0
  44. package/dist-client/viewparts/board-info-template.js +97 -0
  45. package/dist-client/viewparts/board-info-template.js.map +1 -0
  46. package/dist-client/viewparts/board-info.d.ts +5 -0
  47. package/dist-client/viewparts/board-info.js +24 -73
  48. package/dist-client/viewparts/board-info.js.map +1 -1
  49. package/dist-client/viewparts/board-template-builder.d.ts +1 -0
  50. package/dist-client/viewparts/board-template-builder.js +37 -65
  51. package/dist-client/viewparts/board-template-builder.js.map +1 -1
  52. package/dist-client/viewparts/board-template-info.d.ts +3 -0
  53. package/dist-client/viewparts/board-template-info.js +75 -134
  54. package/dist-client/viewparts/board-template-info.js.map +1 -1
  55. package/dist-client/viewparts/group-info-basic.d.ts +8 -3
  56. package/dist-client/viewparts/group-info-basic.js +54 -190
  57. package/dist-client/viewparts/group-info-basic.js.map +1 -1
  58. package/dist-client/viewparts/group-info-import.d.ts +3 -1
  59. package/dist-client/viewparts/group-info-import.js +29 -62
  60. package/dist-client/viewparts/group-info-import.js.map +1 -1
  61. package/dist-client/viewparts/group-info.d.ts +4 -0
  62. package/dist-client/viewparts/group-info.js +21 -55
  63. package/dist-client/viewparts/group-info.js.map +1 -1
  64. package/dist-client/viewparts/info-dialog-styles.d.ts +19 -0
  65. package/dist-client/viewparts/info-dialog-styles.js +194 -0
  66. package/dist-client/viewparts/info-dialog-styles.js.map +1 -0
  67. package/dist-client/viewparts/link-builder.d.ts +3 -0
  68. package/dist-client/viewparts/link-builder.js +87 -81
  69. package/dist-client/viewparts/link-builder.js.map +1 -1
  70. package/dist-client/viewparts/play-group-info-basic.d.ts +4 -3
  71. package/dist-client/viewparts/play-group-info-basic.js +49 -188
  72. package/dist-client/viewparts/play-group-info-basic.js.map +1 -1
  73. package/dist-client/viewparts/play-group-info.d.ts +4 -0
  74. package/dist-client/viewparts/play-group-info.js +22 -53
  75. package/dist-client/viewparts/play-group-info.js.map +1 -1
  76. package/package.json +4 -4
  77. package/translations/en.json +32 -0
  78. package/translations/ja.json +10 -0
  79. package/translations/ko.json +32 -0
  80. package/translations/ms.json +8 -0
  81. package/translations/zh.json +10 -0
@@ -1,65 +1,74 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
+ import '@operato/data-grist';
3
+ import '@material/web/icon/icon.js';
4
+ import '@material/web/button/text-button.js';
5
+ import '../board-list/board-thumb.js';
2
6
  import '../viewparts/board-info.js';
3
7
  import '../viewparts/group-info.js';
4
- import '../board-list/board-tile-list.js';
5
- import '../board-list/group-bar.js';
6
8
  import gql from 'graphql-tag';
7
9
  import { css, html } from 'lit';
8
10
  import { customElement, property, query, state } from 'lit/decorators.js';
11
+ import { DataGrist } from '@operato/data-grist';
9
12
  import { client } from '@operato/graphql';
10
13
  import { i18next, localize } from '@operato/i18n';
11
- import { openOverlay } from '@operato/layout';
14
+ import { openPopup } from '@operato/layout';
12
15
  import { pulltorefresh } from '@operato/pull-to-refresh';
13
16
  import { navigate, PageView, store } from '@operato/shell';
14
- import { ScrollbarStyles } from '@operato/styles';
15
- import InfiniteScrollable from '@operato/utils/mixins/infinite-scrollable.js';
17
+ import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
18
+ import { hasPrivilege } from '@things-factory/auth-base/dist-client/index.js';
16
19
  import { UPDATE_FAVORITES } from '@things-factory/fav-base/client/index.js';
17
- import { swipe } from '@things-factory/utils/src/index.js';
18
20
  import { notify } from '../utils/notify-helper.js';
19
- import { cloneBoard, releaseBoard, revertBoardVersion, createBoard, createGroup, deleteBoard, deleteGroup, fetchBoardList, fetchBoardsUpdatedSince, fetchFavoriteBoardList, fetchMyBoardList, fetchGroupList, updateBoard, updateGroup, importBoards } from '../graphql';
20
- const BoardListPageBase = localize(i18next)(InfiniteScrollable(PageView));
21
+ import { boardMeta } from '../utils/board-meta.js';
22
+ import { BoardCardStyles } from '../styles/board-card-styles.js';
23
+ import { cloneBoard, releaseBoard, revertBoardVersion, createBoard, createGroup, deleteBoard, deleteGroup, fetchBoardList, fetchFavoriteBoardList, fetchMyBoardList, fetchGroupList, updateBoard, updateGroup, importBoards } from '../graphql';
24
+ const BoardListPageBase = localize(i18next)(PageView);
25
+ /*
26
+ * ox-grist (CARD mode) like figure-list-page and board-template-list-page, instead of the
27
+ * board-only board-tile-list fork - see ADR on shared-component-over-app-fork. Drag-reorder comes
28
+ * from `rows.reorderable`, a capability added to data-grist for this (2026-09-23); it dispatches a
29
+ * `reorder` event and leaves persisting it to us, same contract board-tile-list's `reordered` used.
30
+ *
31
+ * Traded away in the move: the delta-merge `incrementalRefresh()` that patched only changed boards
32
+ * in place with a highlight-flash animation. ox-grist's own pagination (`attach-page`, handled
33
+ * internally by data-provider.ts) replaces the hand-rolled infinite scroll, but a group/search change
34
+ * now does a plain full refetch rather than an in-place merge. Worth revisiting if the flash-on-update
35
+ * cue turns out to matter in practice.
36
+ */
21
37
  let BoardListPage = class BoardListPage extends BoardListPageBase {
38
+ constructor() {
39
+ super(...arguments);
40
+ this.groups = [];
41
+ this.favorites = [];
42
+ this.searchText = '';
43
+ this.creatable = false;
44
+ /* Kept only to prefill board-info's "which group" context - the filter itself is the source of
45
+ * truth for what fetchHandler actually asks the server for. */
46
+ this.currentGroupId = '';
47
+ }
22
48
  static { this.styles = [
49
+ /* The frame owns the page shell; the grist is the content it sizes (ADR-0060 decision 8). */
50
+ WorkSurfacePageStyles,
23
51
  ScrollbarStyles,
52
+ CommonGristStyles,
53
+ CommonHeaderStyles,
54
+ /* The card itself - shared with the other catalogs, see styles/board-card-styles.ts. */
55
+ BoardCardStyles,
24
56
  css `
25
- :host {
26
- display: flex;
27
- flex-direction: column;
28
- position: relative;
29
-
30
- overflow: hidden;
31
- }
32
-
33
- board-tile-list {
57
+ ox-filters-form {
34
58
  flex: 1;
35
- overflow-y: auto;
36
- }
37
-
38
- #create {
39
- position: absolute;
40
- bottom: 15px;
41
- right: 16px;
42
59
  }
43
60
  `
44
61
  ]; }
45
62
  get context() {
46
- const groupId = this.groupId;
47
- const group = this.groups && this.groups.find(group => group.id === groupId);
48
63
  return {
49
- title: {
50
- icon: 'dashboard',
51
- text: group
52
- ? 'Group ' + group.name
53
- : groupId == 'favor'
54
- ? 'Favorite'
55
- : groupId == 'mywork'
56
- ? 'My works'
57
- : 'Board List'
58
- },
64
+ /* A plain name, no icon - the rail already carries the icon for this screen, and figure's
65
+ * title bar is bare text too. */
66
+ title: i18next.t('title.board list', { defaultValue: 'Board List' }),
59
67
  search: {
68
+ placeholder: i18next.t('text.search boards by name', { defaultValue: 'Search boards by name' }),
60
69
  handler: search => {
61
70
  this.searchText = search;
62
- this.refreshBoards();
71
+ this.grist?.fetch();
63
72
  // URL에 검색어 반영 → 페이지 복귀 시 유지
64
73
  const url = new URL(window.location.href);
65
74
  if (search) {
@@ -75,82 +84,215 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
75
84
  board_topmenu: true
76
85
  };
77
86
  }
78
- constructor() {
79
- super();
80
- this.groups = [];
81
- this.boards = [];
82
- this.favorites = [];
83
- this.showMainBoardOnly = true;
84
- this.searchText = '';
85
- this._page = 1;
86
- this._total = 0; /* required for infinite-scrolling */
87
- this._updatedBoardIds = [];
88
- /**
89
- * 컨텐츠가 스크롤 영역을 채우지 못하면 다음 페이지를 자동 로드한다.
90
- */
91
- this._filling = false;
92
- this._infiniteScrollOptions.limit = 50;
93
- }
94
87
  render() {
95
88
  return html `
96
- <group-bar
97
- .groups=${this.groups}
98
- .groupId=${this.groupId}
99
- targetPage="board-list"
100
- @info-group=${e => this.onInfoGroup(e.detail)}
101
- @show-all-board-type=${e => this.showAllBoardType(e)}
102
- ></group-bar>
89
+ <ox-grist
90
+ class="page-fill"
91
+ mode="CARD"
92
+ .config=${this.gristConfig}
93
+ .fetchHandler=${this.fetchHandler.bind(this)}
94
+ @reorder=${e => this.onReorderBoards(e.detail)}
95
+ >
96
+ <div slot="headroom">
97
+ <ox-filters-form autofocus without-search></ox-filters-form>
98
+ ${this.groups.some(group => group.id === this.currentGroupId)
99
+ ? html `
100
+ <md-text-button @click=${() => this.onInfoGroup(this.currentGroupId)}>
101
+ <md-icon slot="icon">edit</md-icon>
102
+ ${i18next.t('text.edit group', { defaultValue: 'Edit' })}
103
+ </md-text-button>
104
+ `
105
+ : ''}
106
+ <md-text-button @click=${() => this.onInfoGroup(undefined)}>
107
+ <md-icon slot="icon">create_new_folder</md-icon>
108
+ ${i18next.t('text.new group', { defaultValue: 'New Group' })}
109
+ </md-text-button>
110
+ </div>
111
+ </ox-grist>
103
112
 
104
- <board-tile-list
105
- id="list"
106
- .boards=${this.boards}
107
- .favorites=${this.favorites}
108
- .groups=${this.groups}
109
- .group=${this.groupId}
110
- .updatedIds=${this._updatedBoardIds}
111
- search-text=${this.searchText}
112
- @info-board=${e => this.onInfoBoard(e.detail)}
113
- @scroll=${e => {
114
- ;
115
- this.onScroll(e);
116
- }}
117
- @create-board=${e => this.onCreateBoard(e.detail)}
118
- @refresh-favorites=${e => this.refreshFavorites()}
119
- @reordered=${e => this.onReorderBoards(e.detail)}
120
- creatable
121
- reorderable
122
- ></board-tile-list>
113
+ ${this.creatable
114
+ ? html `
115
+ <button fab title=${i18next.t('text.create board', { defaultValue: 'Create Board' })} @click=${() => navigate('board-create')}>
116
+ <md-icon>add</md-icon>${i18next.t('text.create board', { defaultValue: 'Create Board' })}
117
+ </button>
118
+ `
119
+ : ''}
123
120
  `;
124
121
  }
122
+ async pageInitialized() {
123
+ this.creatable = await hasPrivilege({ privilege: 'mutation', category: 'board', domainOwnerGranted: true });
124
+ this.gristConfig = {
125
+ list: {
126
+ thumbnail: 'thumbnail',
127
+ /* name, then a quieter "v3 · 2 days ago" line - the same two-line rhythm as figure's
128
+ * name-over-type, using what a board actually has (utils/board-meta.ts). */
129
+ fields: ['name', 'meta'],
130
+ details: []
131
+ },
132
+ columns: [
133
+ {
134
+ type: 'image',
135
+ name: 'thumbnail',
136
+ record: {
137
+ editable: false,
138
+ align: 'center',
139
+ renderEmpty: true,
140
+ /* board-thumb.ts owns the picture and the draft badge; info/favorite are the two gutter
141
+ * columns below, in the information area rather than overlaid on the image - see
142
+ * board-thumb.ts's own header comment for why board's case differs from figure's. */
143
+ renderer: (value, column, record) => html `<board-thumb .board=${record}></board-thumb>`
144
+ }
145
+ },
146
+ {
147
+ type: 'string',
148
+ name: 'name',
149
+ record: { editable: false }
150
+ },
151
+ {
152
+ type: 'string',
153
+ name: 'meta',
154
+ record: { editable: false }
155
+ },
156
+ {
157
+ type: 'gutter',
158
+ gutterName: 'button',
159
+ name: 'edit',
160
+ /* Edit first, the star last: the star is the one that can stay lit at rest, so it takes
161
+ * the corner where the eye lands. */
162
+ icon: 'edit',
163
+ quiet: true,
164
+ title: i18next.t('label.edit', { defaultValue: 'Edit' }),
165
+ handlers: {
166
+ click: (_columns, _data, _column, record) => this.onInfoBoard(record)
167
+ }
168
+ },
169
+ {
170
+ type: 'gutter',
171
+ gutterName: 'button',
172
+ name: 'favorite',
173
+ icon: (record) => (record?.favorited ? 'star' : 'star_border'),
174
+ /* A set star is a status and stays; an unset one is an action and waits for hover. */
175
+ quiet: (record) => !record?.favorited,
176
+ title: (record) => i18next.t(record?.favorited ? 'label.unfavorite' : 'label.favorite', {
177
+ defaultValue: record?.favorited ? 'Remove from favorites' : 'Add to favorites'
178
+ }),
179
+ handlers: {
180
+ click: (_columns, _data, _column, record) => this.toggleFavorite(record)
181
+ }
182
+ },
183
+ /*
184
+ * Group and board-type are filter-only facets - they carry no `list.fields`/`list.details`
185
+ * entry, so record-card.ts never draws them on a card. `filters-form`'s delegated `change`
186
+ * listener (filters-form.ts) turns any change into a `fetch-params-change` event that
187
+ * data-provider.ts already listens for, so no extra wiring is needed to refetch.
188
+ */
189
+ {
190
+ type: 'string',
191
+ name: 'group',
192
+ header: i18next.t('field.group', { defaultValue: 'Group' }),
193
+ record: { editable: false },
194
+ filter: {
195
+ type: 'select',
196
+ operator: 'eq',
197
+ value: '',
198
+ label: i18next.t('field.group', { defaultValue: 'Group' }),
199
+ onchange: (value) => {
200
+ this.currentGroupId = value;
201
+ return true;
202
+ },
203
+ options: () => [
204
+ { value: '', display: i18next.t('label.all', { defaultValue: 'All' }) },
205
+ { value: 'favor', display: i18next.t('label.favorite', { defaultValue: 'Favorite' }) },
206
+ { value: 'mywork', display: i18next.t('label.my-works', { defaultValue: 'My works' }) },
207
+ ...this.groups.map(group => ({ value: group.id, display: group.name }))
208
+ ]
209
+ }
210
+ },
211
+ /*
212
+ * Type and state are chips (select-buttons), the way figure's draft/released facet is - a
213
+ * facet with two or three values is read faster as a row of toggles than as a checkbox
214
+ * or a dropdown. Both are multi-select ('in'), and "Main" starts pressed so the default
215
+ * view is the same main-boards-only list as before.
216
+ */
217
+ {
218
+ type: 'select-buttons',
219
+ name: 'type',
220
+ header: i18next.t('field.board-type', { defaultValue: 'Type' }),
221
+ record: { editable: false },
222
+ filter: {
223
+ operator: 'in',
224
+ value: ['main'],
225
+ label: '',
226
+ options: [
227
+ { display: i18next.t('label.main', { defaultValue: 'Main' }), value: 'main' },
228
+ { display: i18next.t('label.sub', { defaultValue: 'Sub' }), value: 'sub' },
229
+ { display: i18next.t('label.popup', { defaultValue: 'Popup' }), value: 'popup' }
230
+ ]
231
+ }
232
+ },
233
+ {
234
+ type: 'select-buttons',
235
+ name: 'state',
236
+ header: i18next.t('field.state', { defaultValue: 'State' }),
237
+ record: { editable: false },
238
+ filter: {
239
+ operator: 'in',
240
+ value: [],
241
+ label: '',
242
+ options: [
243
+ { display: i18next.t('label.draft', { defaultValue: 'Draft' }), value: 'draft' },
244
+ { display: i18next.t('label.released', { defaultValue: 'Released' }), value: 'released' }
245
+ ]
246
+ }
247
+ }
248
+ ],
249
+ rows: {
250
+ selectable: false,
251
+ appendable: false,
252
+ reorderable: true,
253
+ handlers: {
254
+ click: (_columns, _data, _column, record) => navigate(`board-viewer/${record.id}`)
255
+ }
256
+ },
257
+ sorters: []
258
+ };
259
+ this.refresh();
260
+ }
261
+ async fetchHandler({ page = 1, limit = 24, filters = [] }) {
262
+ const groupId = filters.find(f => f.name === 'group')?.value || '';
263
+ const types = filters.find(f => f.name === 'type')?.value || [];
264
+ const states = filters.find(f => f.name === 'state')?.value || [];
265
+ const { items, total } = await this.getBoards({ page, limit, groupId, types, states });
266
+ const favorites = this.favorites || [];
267
+ const records = (items || []).map(board => ({
268
+ ...board,
269
+ favorited: favorites.includes(board.id),
270
+ meta: boardMeta(board)
271
+ }));
272
+ return { total, records };
273
+ }
125
274
  async refresh() {
126
275
  this.groups = (await fetchGroupList()).groups.items;
127
276
  if (this.groups) {
277
+ /* New reference - `options()` on the group filter closes over `this.groups`, but the filter
278
+ * widget only re-reads it if `<ox-grist>`'s own `.config` binding actually changes. */
279
+ this.gristConfig = { ...this.gristConfig };
128
280
  await this.refreshBoards();
129
281
  }
130
282
  }
131
- showAllBoardType(e) {
132
- this.showMainBoardOnly = !e.detail;
133
- this.refreshBoards();
134
- }
135
- async getBoards({ page = 1, limit = this._infiniteScrollOptions.limit } = {}) {
136
- if (this.groupId && this.groupId == 'favor') {
137
- return await this.getFavoriteBoards({
138
- page,
139
- limit
140
- });
283
+ async getBoards({ page = 1, limit = 24, groupId = '', types = [], states = [] } = {}) {
284
+ if (groupId === 'favor') {
285
+ return await this.getFavoriteBoards({ page, limit, types, states });
141
286
  }
142
- else if (this.groupId && this.groupId == 'mywork') {
143
- return await this.getMyBoards({
144
- page,
145
- limit
146
- });
287
+ else if (groupId === 'mywork') {
288
+ return await this.getMyBoards({ page, limit, types, states });
147
289
  }
148
290
  const filters = [];
149
- if (this.groupId) {
291
+ if (groupId) {
150
292
  filters.push({
151
293
  name: 'groupId',
152
294
  operator: 'eq',
153
- value: this.groupId
295
+ value: groupId
154
296
  });
155
297
  }
156
298
  if (this.searchText) {
@@ -160,11 +302,18 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
160
302
  value: '%' + this.searchText + '%'
161
303
  });
162
304
  }
163
- if (this.showMainBoardOnly) {
305
+ if (types.length) {
164
306
  filters.push({
165
307
  name: 'type',
166
- operator: 'eq',
167
- value: 'main'
308
+ operator: 'in',
309
+ value: types
310
+ });
311
+ }
312
+ if (states.length) {
313
+ filters.push({
314
+ name: 'state',
315
+ operator: 'in',
316
+ value: states
168
317
  });
169
318
  }
170
319
  const listParam = {
@@ -190,8 +339,16 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
190
339
  * favor/mywork 같이 "search text + pagination"만 받는 간단한 리스트 조회 공용 헬퍼.
191
340
  * 그룹/타입 필터 등 복합 조건을 쓰는 일반 목록(getBoards)과 구분해 중복 제거.
192
341
  */
193
- async _fetchSimpleBoardList(fetcher, resultKey, { page, limit }) {
342
+ async _fetchSimpleBoardList(fetcher, resultKey, { page, limit, types = [], states = [] }) {
343
+ /* The type and state chips apply here too - the server narrows these lists with the same
344
+ * filters as the plain board list, so a pressed chip means the same thing in every view. */
194
345
  const filters = [];
346
+ if (types.length) {
347
+ filters.push({ name: 'type', operator: 'in', value: types });
348
+ }
349
+ if (states.length) {
350
+ filters.push({ name: 'state', operator: 'in', value: states });
351
+ }
195
352
  if (this.searchText) {
196
353
  filters.push({
197
354
  name: 'name',
@@ -202,129 +359,30 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
202
359
  const result = await fetcher({ filters, pagination: { page, limit } });
203
360
  return result[resultKey];
204
361
  }
205
- async getFavoriteBoards({ page = 1, limit = this._infiniteScrollOptions.limit } = {}) {
206
- return this._fetchSimpleBoardList(fetchFavoriteBoardList, 'favoriteBoards', { page, limit });
362
+ async getFavoriteBoards({ page = 1, limit = 24, types = [], states = [] } = {}) {
363
+ return this._fetchSimpleBoardList(fetchFavoriteBoardList, 'favoriteBoards', { page, limit, types, states });
207
364
  }
208
- async getMyBoards({ page = 1, limit = this._infiniteScrollOptions.limit } = {}) {
209
- return this._fetchSimpleBoardList(fetchMyBoardList, 'boardsCreatedByMe', { page, limit });
365
+ async getMyBoards({ page = 1, limit = 24, types = [], states = [] } = {}) {
366
+ return this._fetchSimpleBoardList(fetchMyBoardList, 'boardsCreatedByMe', { page, limit, types, states });
210
367
  }
211
368
  async refreshBoards() {
212
369
  if (!this.groups) {
213
370
  await this.refresh();
214
371
  return;
215
372
  }
216
- const { items: boards, total } = await this.getBoards();
217
- this.boards = boards;
218
- this._page = 1;
219
- this._total = total;
220
- this._lastFetchTime = new Date();
221
373
  this.updateContext();
222
- this.scrollTargetEl.style.transition = '';
223
- this.scrollTargetEl.style.transform = `translate3d(0, 0, 0)`;
224
- this.fillViewport();
225
- }
226
- async fillViewport() {
227
- if (this._filling)
228
- return;
229
- this._filling = true;
230
- try {
231
- const el = this.scrollTargetEl;
232
- if (!el)
233
- return;
234
- const { limit } = this._infiniteScrollOptions;
235
- while (this._page < this._total / limit && el.scrollHeight <= el.clientHeight) {
236
- await this.appendBoards();
237
- await this.updateComplete;
238
- }
374
+ this.grist?.fetch();
375
+ if (this.grist) {
376
+ this.grist.style.transition = '';
377
+ this.grist.style.transform = `translate3d(0, 0, 0)`;
239
378
  }
240
- finally {
241
- this._filling = false;
242
- }
243
- }
244
- /**
245
- * 기존 목록을 유지하면서 변경된 보드만 갱신한다.
246
- * 스크롤 위치가 보존된다.
247
- */
248
- async incrementalRefresh() {
249
- if (!this._lastFetchTime || this.boards.length === 0) {
250
- return this.refreshBoards();
251
- }
252
- // boardsUpdatedSince 는 *전역* 델타라, 즐겨찾기/내작업/검색 모드는 소속·검색일치를
253
- // 클라이언트에서 판별할 수 없다 → 정확성 위해 전체 갱신으로 폴백.
254
- // (일반 그룹/전체 보기에서만 증분 병합으로 스크롤을 유지한다.)
255
- if (this.groupId === 'favor' || this.groupId === 'mywork' || this.searchText) {
256
- return this.refreshBoards();
257
- }
258
- const updatedBoards = await fetchBoardsUpdatedSince(this._lastFetchTime);
259
- if (updatedBoards && updatedBoards.length > 0) {
260
- const updatedMap = new Map(updatedBoards.map((b) => [b.id, b]));
261
- const existingIds = new Set(this.boards.map(b => b.id));
262
- const changedIds = [];
263
- // 현재 보기(그룹/타입 필터)에 속하는 살아있는 보드인지
264
- const belongsToView = (b) => !b.deletedAt &&
265
- (!this.groupId || b.group?.id === this.groupId) &&
266
- (!this.showMainBoardOnly || b.type === 'main');
267
- // 1) 기존 항목: 변경분 갱신, 삭제·그룹이동·타입이탈은 제거
268
- const merged = this.boards
269
- .map(board => {
270
- const updated = updatedMap.get(board.id);
271
- if (!updated)
272
- return board;
273
- if (!belongsToView(updated))
274
- return null;
275
- changedIds.push(updated.id);
276
- return updated;
277
- })
278
- .filter(Boolean);
279
- const removedCount = this.boards.length - merged.length;
280
- // 2) 신규: 기존 리스트에 없고 현재 보기에 속하는 보드를 삽입 (새로 만든 보드가 여기 해당)
281
- const inserted = updatedBoards.filter((b) => !existingIds.has(b.id) && belongsToView(b));
282
- for (const b of inserted)
283
- changedIds.push(b.id);
284
- // 3) 서버 정렬(sortOrder asc, name asc) 유지 — 새 항목이 제 위치에 끼어들고 기존 순서/스크롤 보존
285
- this.boards = [...merged, ...inserted].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0) ||
286
- String(a.name ?? '').localeCompare(String(b.name ?? '')));
287
- this._total += inserted.length - removedCount;
288
- this._updatedBoardIds = changedIds;
289
- }
290
- this._lastFetchTime = new Date();
291
- }
292
- async appendBoards() {
293
- if (!this.groups) {
294
- await this.refresh();
295
- return;
296
- }
297
- const { items: appendedBoards, total } = await this.getBoards({ page: this._page + 1 });
298
- const existingIds = new Set(this.boards.map(b => b.id));
299
- const newBoards = appendedBoards.filter(b => !existingIds.has(b.id));
300
- this.boards = [...this.boards, ...newBoards];
301
- this._page = this._page + 1;
302
- this._total = total;
303
- this._lastFetchTime = new Date();
304
- }
305
- async scrollAction() {
306
- return this.appendBoards();
307
- }
308
- async pageInitialized() {
309
- this.refresh();
310
379
  }
311
380
  async pageUpdated(changes, lifecycle) {
312
381
  if (this.active) {
313
- const prevGroupId = this.groupId;
314
- const prevSearchText = this.searchText;
315
- this.page = lifecycle.page;
316
- this.groupId = lifecycle.resourceId;
317
- this.searchText = lifecycle.params?.search || '';
318
- await this.updateComplete;
319
- // 검색어가 변경되었으면 전체 갱신
320
- // 같은 그룹으로 복귀하고 기존 데이터가 있으면 변경분만 갱신 (스크롤 유지)
321
- if (prevSearchText !== this.searchText) {
322
- this.refreshBoards();
323
- }
324
- else if (prevGroupId === this.groupId && this.boards.length > 0 && this._lastFetchTime) {
325
- this.incrementalRefresh();
326
- }
327
- else {
382
+ const searchText = lifecycle.params?.search || '';
383
+ if (searchText !== this.searchText) {
384
+ this.searchText = searchText;
385
+ await this.updateComplete;
328
386
  this.refreshBoards();
329
387
  }
330
388
  }
@@ -340,58 +398,35 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
340
398
  }
341
399
  _applyState() {
342
400
  const __state__ = store.getState();
343
- this.favorites = __state__.favorite.favorites;
401
+ const favorites = __state__.favorite.favorites;
402
+ /*
403
+ * `store.subscribe` fires on every dispatch anywhere in the app, not just favorite changes -
404
+ * `favorited` is folded into each record inside `fetchHandler`, so refreshing it means a
405
+ * refetch, and an unconditional one here span a real loop: fetch() completing dispatches through
406
+ * this same store, which re-enters this callback, which calls fetch() again.
407
+ */
408
+ const changed = JSON.stringify(favorites) !== JSON.stringify(this.favorites);
409
+ this.favorites = favorites;
410
+ if (changed) {
411
+ this.grist?.fetch();
412
+ }
344
413
  }
345
414
  firstUpdated() {
346
- const list = this.scrollTargetEl;
415
+ /* Group used to be a route segment, and swiping moved between routes - now it is one filter
416
+ * among others in the same list, so there is no longer a sequence of screens to swipe between. */
347
417
  pulltorefresh({
348
418
  container: this.renderRoot,
349
- scrollable: list,
419
+ scrollable: this.grist,
350
420
  refresh: () => {
351
421
  return this.refresh();
352
422
  }
353
423
  });
354
- swipe({
355
- container: list,
356
- animates: {
357
- dragging: async (d, opts) => {
358
- const groups = [{ id: '' }, { id: 'favor' }, ...this.groups];
359
- const currentIndex = groups.findIndex(group => group.id == this.groupId);
360
- if ((d > 0 && currentIndex <= 0) || (d < 0 && currentIndex >= groups.length - 1)) {
361
- /* TODO blocked gesture */
362
- return false;
363
- }
364
- list.style.transform = `translate3d(${d}px, 0, 0)`;
365
- },
366
- aborting: async (opts) => {
367
- list.style.transition = 'transform 0.3s';
368
- list.style.transform = `translate3d(0, 0, 0)`;
369
- setTimeout(() => {
370
- list.style.transition = '';
371
- });
372
- },
373
- swiping: async (d, opts) => {
374
- const groups = [{ id: '' }, { id: 'favor' }, ...this.groups];
375
- const currentIndex = groups.findIndex(group => group.id == this.groupId);
376
- if ((d > 0 && currentIndex <= 0) || (d < 0 && currentIndex >= groups.length - 1)) {
377
- list.style.transition = '';
378
- list.style.transform = `translate3d(0, 0, 0)`;
379
- }
380
- else {
381
- list.style.transition = 'transform 0.3s';
382
- list.style.transform = `translate3d(${d < 0 ? '-100%' : '100%'}, 0, 0)`;
383
- navigate(`${this.page}/${groups[currentIndex + (d < 0 ? 1 : -1)].id}${this.searchText ? '?search=' + this.searchText : ''}`);
384
- }
385
- }
386
- }
387
- });
388
424
  }
389
425
  async onInfoBoard(board) {
390
- openOverlay('viewpart-info', {
391
- template: html `
426
+ openPopup(html `
392
427
  <board-info
393
428
  .board=${board}
394
- .groupId=${this.groupId}
429
+ .groupId=${this.currentGroupId}
395
430
  @clone-board=${e => this.onCloneBoard(e.detail)}
396
431
  @release-board=${e => this.onReleaseBoard(e.detail)}
397
432
  @revert-board-version=${e => this.onRevertBoardVersion(e.detail)}
@@ -401,12 +436,11 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
401
436
  @leave-playgroup=${e => this.onLeavePlayGroup(e.detail)}
402
437
  @register-template=${e => this.onRegisterTemplate(e.detail)}
403
438
  ></board-info>
404
- `
405
- });
439
+ `, { backdrop: true, size: 'medium', closable: true, title: board.name });
406
440
  }
407
441
  async onInfoGroup(groupId) {
408
- openOverlay('viewpart-info', {
409
- template: html `
442
+ const group = this.groups.find(group => group.id == groupId);
443
+ openPopup(html `
410
444
  <group-info
411
445
  .groupId=${groupId}
412
446
  @update-group=${e => this.onUpdateGroup(e.detail)}
@@ -414,7 +448,11 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
414
448
  @create-group=${e => this.onCreateGroup(e.detail)}
415
449
  @import-boards=${e => this.onImportBoards(e.detail)}
416
450
  ></group-info>
417
- `
451
+ `, {
452
+ backdrop: true,
453
+ size: 'small',
454
+ closable: true,
455
+ title: group?.name || i18next.t('text.new group', { defaultValue: 'New Group' })
418
456
  });
419
457
  }
420
458
  async onCreateGroup(group) {
@@ -457,13 +495,12 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
457
495
  }
458
496
  this.refresh();
459
497
  }
460
- async onReorderBoards({ boardIds, movedId }) {
498
+ async onReorderBoards({ ids, movedId }) {
461
499
  if (!movedId)
462
500
  return;
463
- const newIndex = boardIds.indexOf(movedId);
464
- const prevId = newIndex > 0 ? boardIds[newIndex - 1] : undefined;
465
- const nextId = newIndex < boardIds.length - 1 ? boardIds[newIndex + 1] : undefined;
466
- this.boards = boardIds.map(id => this.boards.find(b => b.id === id)).filter(Boolean);
501
+ const newIndex = ids.indexOf(movedId);
502
+ const prevId = newIndex > 0 ? ids[newIndex - 1] : undefined;
503
+ const nextId = newIndex < ids.length - 1 ? ids[newIndex + 1] : undefined;
467
504
  try {
468
505
  await client.mutate({
469
506
  mutation: gql `
@@ -509,7 +546,7 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
509
546
  catch (ex) {
510
547
  notify('error', ex, ex);
511
548
  }
512
- this.incrementalRefresh();
549
+ this.refreshBoards();
513
550
  }
514
551
  async onReleaseBoard(board) {
515
552
  try {
@@ -519,7 +556,7 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
519
556
  catch (ex) {
520
557
  notify('error', ex, ex);
521
558
  }
522
- this.incrementalRefresh();
559
+ this.refreshBoards();
523
560
  }
524
561
  async onRevertBoardVersion({ id, version }) {
525
562
  try {
@@ -529,7 +566,7 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
529
566
  catch (ex) {
530
567
  notify('error', ex, ex);
531
568
  }
532
- this.incrementalRefresh();
569
+ this.refreshBoards();
533
570
  }
534
571
  async onUpdateBoard(board) {
535
572
  try {
@@ -539,7 +576,7 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
539
576
  catch (ex) {
540
577
  notify('error', ex, ex);
541
578
  }
542
- this.incrementalRefresh();
579
+ this.refreshBoards();
543
580
  }
544
581
  async onDeleteBoard(board) {
545
582
  try {
@@ -549,7 +586,7 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
549
586
  catch (ex) {
550
587
  notify('error', ex, ex);
551
588
  }
552
- this.incrementalRefresh();
589
+ this.refreshBoards();
553
590
  }
554
591
  async onJoinPlayGroup({ board, playGroup }) {
555
592
  try {
@@ -621,6 +658,78 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
621
658
  notify('error', ex, ex);
622
659
  }
623
660
  }
661
+ async toggleFavorite(board) {
662
+ const wasFavorite = (this.favorites || []).includes(board.id);
663
+ /*
664
+ * The star answers the click at once; the server round trip only confirms it. Before, nothing
665
+ * moved until the mutation, the favorites re-query, the store dispatch and a full refetch had
666
+ * all come back - a click with no reaction reads as a click that did not land.
667
+ *
668
+ * `favorites` is set locally too, so the store update that follows carries the same list and
669
+ * `_applyState` does not trigger the refetch that would repaint every card.
670
+ */
671
+ this.favorites = wasFavorite
672
+ ? (this.favorites || []).filter(id => id !== board.id)
673
+ : [...(this.favorites || []), board.id];
674
+ this.patchRecord(board.id, { favorited: !wasFavorite });
675
+ try {
676
+ if (wasFavorite) {
677
+ await this.removeFavorite(board.id);
678
+ }
679
+ else {
680
+ await this.addFavorite(board.id);
681
+ }
682
+ /* In the Favorite view an unstarred board no longer belongs on the page: refetch so it leaves. */
683
+ if (this.currentGroupId === 'favor') {
684
+ this.grist?.fetch();
685
+ }
686
+ }
687
+ catch (ex) {
688
+ /* Put it back the way it was and say why. */
689
+ this.favorites = wasFavorite
690
+ ? [...(this.favorites || []), board.id]
691
+ : (this.favorites || []).filter(id => id !== board.id);
692
+ this.patchRecord(board.id, { favorited: wasFavorite });
693
+ notify('error', ex?.message || String(ex));
694
+ }
695
+ }
696
+ /** Replaces one record in the grist in place, so only that card re-renders. */
697
+ patchRecord(id, patch) {
698
+ const grist = this.grist;
699
+ const data = grist?.data;
700
+ if (!data?.records) {
701
+ return;
702
+ }
703
+ grist.data = {
704
+ ...data,
705
+ records: data.records.map(record => (record.id === id ? { ...record, ...patch } : record))
706
+ };
707
+ }
708
+ async removeFavorite(boardId) {
709
+ await client.mutate({
710
+ mutation: gql `
711
+ mutation {
712
+ deleteFavorite(routing: "${boardId}")
713
+ }
714
+ `
715
+ });
716
+ this.refreshFavorites();
717
+ }
718
+ async addFavorite(boardId) {
719
+ await client.mutate({
720
+ mutation: gql `
721
+ mutation {
722
+ createFavorite(favorite: {
723
+ routing: "${boardId}"
724
+ }) {
725
+ id
726
+ routing
727
+ }
728
+ }
729
+ `
730
+ });
731
+ this.refreshFavorites();
732
+ }
624
733
  async refreshFavorites() {
625
734
  const { data } = await client.query({
626
735
  query: gql `
@@ -639,41 +748,36 @@ let BoardListPage = class BoardListPage extends BoardListPageBase {
639
748
  });
640
749
  }
641
750
  };
642
- __decorate([
643
- property({ type: String }),
644
- __metadata("design:type", String)
645
- ], BoardListPage.prototype, "groupId", void 0);
646
751
  __decorate([
647
752
  property({ type: Array }),
648
753
  __metadata("design:type", Array)
649
754
  ], BoardListPage.prototype, "groups", void 0);
650
- __decorate([
651
- property({ type: Array }),
652
- __metadata("design:type", Array)
653
- ], BoardListPage.prototype, "boards", void 0);
654
755
  __decorate([
655
756
  property({ type: Array }),
656
757
  __metadata("design:type", Array)
657
758
  ], BoardListPage.prototype, "favorites", void 0);
658
759
  __decorate([
659
- state(),
660
- __metadata("design:type", Boolean)
661
- ], BoardListPage.prototype, "showMainBoardOnly", void 0);
760
+ property({ type: Object }),
761
+ __metadata("design:type", Object)
762
+ ], BoardListPage.prototype, "gristConfig", void 0);
662
763
  __decorate([
663
764
  state(),
664
765
  __metadata("design:type", String)
665
766
  ], BoardListPage.prototype, "searchText", void 0);
666
767
  __decorate([
667
- query('board-tile-list'),
668
- __metadata("design:type", HTMLElement)
669
- ], BoardListPage.prototype, "scrollTargetEl", void 0);
768
+ state(),
769
+ __metadata("design:type", Object)
770
+ ], BoardListPage.prototype, "creatable", void 0);
670
771
  __decorate([
671
772
  state(),
672
- __metadata("design:type", Array)
673
- ], BoardListPage.prototype, "_updatedBoardIds", void 0);
773
+ __metadata("design:type", String)
774
+ ], BoardListPage.prototype, "currentGroupId", void 0);
775
+ __decorate([
776
+ query('ox-grist'),
777
+ __metadata("design:type", DataGrist)
778
+ ], BoardListPage.prototype, "grist", void 0);
674
779
  BoardListPage = __decorate([
675
- customElement('board-list-page'),
676
- __metadata("design:paramtypes", [])
780
+ customElement('board-list-page')
677
781
  ], BoardListPage);
678
782
  export { BoardListPage };
679
783
  //# sourceMappingURL=board-list-page.js.map