vimp-engine 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vimp-engine",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "VIMP — движок-приложение (мастер, P2P-транспорт, Worker-хост, мета, MVC-каркас клиента)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,6 +29,7 @@ export default class GamesCtrl {
29
29
  vp.on('submit', 'submit', this);
30
30
  vp.on('update-version', 'updateVersion', this);
31
31
  vp.on('delete', 'remove', this);
32
+ vp.on('restore', 'restore', this);
32
33
  vp.on('filter', 'filter', this);
33
34
  vp.on('stage', 'stage', this);
34
35
  vp.on('approve', 'approve', this);
@@ -81,6 +82,10 @@ export default class GamesCtrl {
81
82
  this._model.remove(id, scope);
82
83
  }
83
84
 
85
+ restore({ id }) {
86
+ this._model.restore(id);
87
+ }
88
+
84
89
  filter(status) {
85
90
  this._model.setFilter(status);
86
91
  }
@@ -156,7 +156,11 @@ export default class GamesModel {
156
156
  }
157
157
 
158
158
  // удаление игры. scope решает, чей список перечитать: карточка автора и
159
- // очередь модерации показывают разные проекции одной строки
159
+ // очередь модерации показывают разные проекции одной строки.
160
+ //
161
+ // Удаление обратимо: игра уходит в графу Deleted очереди модерации и
162
+ // возвращается оттуда `restore` — подтверждения второй кнопкой оно
163
+ // поэтому не требует
160
164
  async remove(id, scope = 'mine') {
161
165
  const { ok, json } = await this._request(this._config.urls.remove(id), {
162
166
  method: 'DELETE',
@@ -169,12 +173,34 @@ export default class GamesModel {
169
173
 
170
174
  if (scope === 'admin') {
171
175
  await this.loadAdmin();
176
+
177
+ // после loadAdmin, а не до: renderAdmin чистит строку панели (см.
178
+ // moderate). Снятая с раздачи последняя игра оставляет лобби без
179
+ // каталога, и сказать об этом обязан тот же тик
180
+ if (json?.warning) {
181
+ this.publisher.emit('warning', { scope: 'admin', code: json.warning });
182
+ }
183
+
172
184
  return;
173
185
  }
174
186
 
175
187
  await this.loadMine();
176
188
  }
177
189
 
190
+ // возврат мягко удалённой игры из графы Deleted (только админ)
191
+ async restore(id) {
192
+ const { ok, json } = await this._request(this._config.urls.restore(id), {
193
+ method: 'POST',
194
+ });
195
+
196
+ if (!ok) {
197
+ this._fail('admin', json);
198
+ return;
199
+ }
200
+
201
+ await this.loadAdmin();
202
+ }
203
+
178
204
  // «Test»: мастер качает версию и кладёт её в каталог не раздаваемой.
179
205
  // Манифест едет наружу — по нему вкладка админа поднимает комнату
180
206
  async stage(id, version) {
@@ -218,9 +244,17 @@ export default class GamesModel {
218
244
  await this.moderate(id, { authorNick: nick === '' ? null : nick });
219
245
  }
220
246
 
247
+ // графа, в которой стоит игра. Мягко удалённая уходит в свою графу
248
+ // ЦЕЛИКОМ, сохраняя прежний статус: восстановление возвращает её туда,
249
+ // где она была, а показывать её ещё и в Pending/Published значило бы
250
+ // предлагать модерировать то, чего у игроков уже нет
251
+ _bucketOf(game) {
252
+ return game.deletedAt ? 'deleted' : game.status;
253
+ }
254
+
221
255
  _emitAdmin() {
222
256
  this.publisher.emit('admin-changed', {
223
- games: this._all.filter(game => game.status === this._filter),
257
+ games: this._all.filter(game => this._bucketOf(game) === this._filter),
224
258
  filter: this._filter,
225
259
  versions: this._versions,
226
260
  });
@@ -288,8 +288,10 @@ export default class GamesView {
288
288
  item.appendChild(version);
289
289
  item.appendChild(send);
290
290
  item.appendChild(
291
- this._deleteButton(() =>
292
- this.publisher.emit('delete', { id: game.id, scope: 'mine' }),
291
+ this._button(
292
+ 'Delete',
293
+ () => this.publisher.emit('delete', { id: game.id, scope: 'mine' }),
294
+ 'games-delete-btn',
293
295
  ),
294
296
  );
295
297
  this._mineList.appendChild(item);
@@ -337,6 +339,21 @@ export default class GamesView {
337
339
  'games-item-title',
338
340
  ),
339
341
  );
342
+
343
+ // Мягко удалённая игра модерации не подлежит: у игроков её уже нет, а
344
+ // решение по ней приняли. Из всей карточки ей нужны срок полного
345
+ // удаления и возврат — остальные кнопки правили бы то, чего не видно
346
+ if (game.deletedAt) {
347
+ item.appendChild(this._line(`Author: ${game.authorNick ?? game.authorUserId ?? '—'}`));
348
+ this._appendRepo(item, game.repoUrl);
349
+ item.appendChild(this._line(this._statusLine(game), 'games-purge-line'));
350
+ item.appendChild(
351
+ this._button('Restore', () => this.publisher.emit('restore', { id: game.id })),
352
+ );
353
+
354
+ return item;
355
+ }
356
+
340
357
  // у игр, засеянных миграцией, автора нет вовсе: без запасного прочерка
341
358
  // в строке печаталось бы литеральное "null"
342
359
  item.appendChild(this._line(`Author: ${game.authorNick ?? game.authorUserId ?? '—'}`));
@@ -408,7 +425,11 @@ export default class GamesView {
408
425
  ),
409
426
  );
410
427
  item.appendChild(
411
- this._deleteButton(() => this.publisher.emit('delete', { id: game.id, scope: 'admin' })),
428
+ this._button(
429
+ 'Delete',
430
+ () => this.publisher.emit('delete', { id: game.id, scope: 'admin' }),
431
+ 'games-delete-btn',
432
+ ),
412
433
  );
413
434
 
414
435
  return item;
@@ -438,6 +459,16 @@ export default class GamesView {
438
459
  const status = STATUS_TITLES[game.status] ?? game.status;
439
460
  const date = game.createdAt ? new Date(game.createdAt).toLocaleDateString() : '';
440
461
 
462
+ // у удалённой игры прежний статус — след того, куда её вернёт Restore, —
463
+ // а вести отсчёт человеку нужно от срока полного удаления: только он
464
+ // ограничен во времени
465
+ if (game.deletedAt) {
466
+ const deleted = new Date(game.deletedAt).toLocaleDateString();
467
+ const purge = game.purgeAt ? new Date(game.purgeAt).toLocaleDateString() : '—';
468
+
469
+ return `Deleted ${deleted} (was ${status}); removed for good on ${purge}`;
470
+ }
471
+
441
472
  return date ? `Status: ${status} (submitted ${date})` : `Status: ${status}`;
442
473
  }
443
474
 
@@ -459,34 +490,17 @@ export default class GamesView {
459
490
  });
460
491
  }
461
492
 
462
- // Удаление необратимо и уносит рейтинги игры, поэтому кнопка требует
463
- // второго нажатия: первое переводит её в «Confirm delete», второе публикует
464
- // событие. Повторная отрисовка списка возвращает её в исходное состояние —
465
- // незавершённое подтверждение не переживает перерисовку
466
- _deleteButton(onConfirm) {
467
- const btn = this._button('Delete', () => {
468
- if (btn.dataset.armed !== '1') {
469
- btn.dataset.armed = '1';
470
- btn.value = 'Confirm delete';
471
- btn.classList.add('games-delete-armed');
472
- return;
473
- }
474
-
475
- onConfirm();
476
- });
477
-
478
- btn.className = 'games-delete-btn';
479
-
480
- return btn;
481
- }
482
-
483
- _button(value, onclick) {
493
+ _button(value, onclick, className) {
484
494
  const btn = document.createElement('input');
485
495
 
486
496
  btn.type = 'button';
487
497
  btn.value = value;
488
498
  btn.onclick = onclick;
489
499
 
500
+ if (className) {
501
+ btn.className = className;
502
+ }
503
+
490
504
  return btn;
491
505
  }
492
506
 
@@ -217,9 +217,9 @@ body.hide-cursor {
217
217
  background: rgba(25, 25, 25, 0.9);
218
218
  border-radius: 5px;
219
219
  border: 3px solid #fff;
220
- overflow: hidden;
220
+ overflow: auto;
221
221
  width: 90%;
222
- max-width: 550px;
222
+ max-width: 500px;
223
223
  font-size: 10px;
224
224
  line-height: 12px;
225
225
  color: #fff;
@@ -438,7 +438,7 @@ body.hide-cursor {
438
438
  background-color: #333;
439
439
  border: 1px solid #eee;
440
440
  padding: 10px;
441
- margin: 10px auto 5px;
441
+ margin: 5px auto 0;
442
442
  text-shadow: 1px 1px 1px #000;
443
443
  font-size: 12px;
444
444
  line-height: 15px;
@@ -549,15 +549,11 @@ body.hide-cursor {
549
549
  padding: 0 10px;
550
550
  }
551
551
 
552
- /* ширина — контентная область правой панели лобби (её ширина минус
553
- собственные поля .lobby-browser-inner), под которой футер и стоит;
554
- max-width, а не width: в одну колонку он обязан ужиматься по экрану */
555
552
  #lobby-footer {
556
- width: 100%;
557
- max-width: calc(var(--lobby-browser-width) - 2 * var(--lobby-panel-pad));
553
+ width: calc(var(--lobby-width) + var(--lobby-panel-pad));
558
554
  font-size: 10px;
559
555
  padding: 0;
560
- margin: 0 var(--lobby-panel-pad) 0 0;
556
+ margin: 0 calc(var(--lobby-panel-pad) / 2) 0 0;
561
557
  }
562
558
 
563
559
  /* три равные ячейки: середину держит не space-between, а сама равность —
@@ -670,11 +666,7 @@ body.hide-cursor {
670
666
  /*lobby*/
671
667
 
672
668
  #lobby {
673
- /* ширины панелей и их внутренние поля: по ним же считается ширина футера
674
- (секция auth выше), поэтому числа живут в одном месте — иначе смена
675
- ширины любой панели молча разъезжает вёрстку */
676
- --lobby-setup-width: 410px;
677
- --lobby-browser-width: 430px;
669
+ --lobby-width: 370px;
678
670
  --lobby-panel-pad: 10px;
679
671
 
680
672
  background-color: #222;
@@ -832,12 +824,12 @@ body.hide-cursor {
832
824
  высоты более высокой панели */
833
825
  .lobby-grid {
834
826
  display: flex;
835
- gap: 10px;
827
+ gap: 5px;
836
828
  justify-content: center;
837
829
  }
838
830
 
839
831
  #lobby-setup-panel {
840
- width: var(--lobby-setup-width);
832
+ width: var(--lobby-width);
841
833
  }
842
834
 
843
835
  /* высоту правой .card задаёт форма слева: обе панели тянутся до высоты
@@ -846,9 +838,8 @@ body.hide-cursor {
846
838
  вынесен в абсолютный слой .lobby-browser-inner (его высота не участвует в
847
839
  расчёте высоты строки) */
848
840
  #lobby-browser-panel {
849
- width: var(--lobby-browser-width);
841
+ width: var(--lobby-width);
850
842
  position: relative;
851
- padding: 0;
852
843
  }
853
844
 
854
845
  .lobby-browser-inner {
@@ -1147,16 +1138,16 @@ body.hide-cursor {
1147
1138
  color: #eee;
1148
1139
  }
1149
1140
 
1150
- /* удаление необратимо: взведённое подтверждение обязано отличаться от
1151
- первого нажатия не только подписью */
1141
+ /* удаление обратимо (игра уходит в графу Deleted на 30 суток), но выделяется
1142
+ среди соседних кнопок: рядом стоит правка версии, а не действие того же веса */
1152
1143
  .games-delete-btn {
1153
1144
  color: #d66;
1154
1145
  }
1155
1146
 
1156
- .games-delete-armed {
1157
- background: #802020;
1158
- border-color: #d66;
1159
- color: #fee;
1147
+ /* срок полного удаления — единственное, что в карточке удалённой игры
1148
+ ограничено во времени */
1149
+ .games-purge-line {
1150
+ color: #d66;
1160
1151
  }
1161
1152
 
1162
1153
  /* кнопки входа в панель стоят рядом с Sign out в бейдже пользователя */
@@ -218,19 +218,26 @@ export default {
218
218
  staged: '/admin/games/manifest.json',
219
219
  // «Test»: скачать версию и положить её в каталог не раздаваемой
220
220
  stage: id => `/admin/games/${encodeURIComponent(id)}/stage`,
221
+ // возврат мягко удалённой игры из графы Deleted (только админ)
222
+ restore: id => `/admin/games/${encodeURIComponent(id)}/restore`,
221
223
  // решение модератора
222
224
  moderate: id => `/admin/games/${encodeURIComponent(id)}`,
223
225
  // что опубликовано в npm — индикатор «есть версия новее»
224
226
  versions: id => `/admin/games/${encodeURIComponent(id)}/versions`,
225
227
  },
226
228
 
227
- // фильтры очереди модерации: id статуса реестра -> подпись кнопки.
228
- // Значения обязаны совпадать со статусами auth-сервиса
229
+ // фильтры («графы») очереди модерации: id -> подпись кнопки. Первые
230
+ // четыре — статусы реестра, и значения обязаны совпадать со статусами
231
+ // auth-сервиса. Пятая графа стоит особняком: 'deleted' статусом НЕ
232
+ // является (мягко удалённая игра сохраняет свой прежний статус, иначе
233
+ // восстанавливать было бы не во что) — под неё попадает всё с
234
+ // непустым deletedAt, см. GamesModel._bucketOf
229
235
  statuses: [
230
236
  { id: 'pending', title: 'Pending' },
231
237
  { id: 'approved', title: 'Published' },
232
238
  { id: 'rejected', title: 'Rejected' },
233
239
  { id: 'disabled', title: 'Disabled' },
240
+ { id: 'deleted', title: 'Deleted' },
234
241
  ],
235
242
  defaultStatus: 'pending',
236
243