@elice/material-exercise 1.250618.0 → 1.250711.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.
Files changed (23) hide show
  1. package/cjs/components/material-exercise/exercise-file-editor/ExerciseFileEditor.d.ts +3 -5
  2. package/cjs/components/material-exercise/exercise-file-editor/ExerciseFileEditor.js +58 -20
  3. package/cjs/components/material-exercise/exercise-file-editor/ExerciseFileReadOnlyBanner.d.ts +5 -2
  4. package/cjs/components/material-exercise/exercise-file-editor/ExerciseFileReadOnlyBanner.js +1 -8
  5. package/cjs/components/material-exercise/exercise-file-editor/locales/en.json.js +1 -1
  6. package/cjs/components/material-exercise/exercise-file-editor/locales/ja.json.js +1 -1
  7. package/cjs/components/material-exercise/exercise-file-editor/locales/ko.json.js +1 -1
  8. package/cjs/components/material-exercise/exercise-file-editor/locales/th.json.js +1 -1
  9. package/cjs/components/material-exercise/exercise-runner/ExerciseRunnerControllerStatusMessage.js +5 -0
  10. package/cjs/hooks/useUsercodeEditWebSocket.d.ts +6 -1
  11. package/cjs/hooks/useUsercodeEditWebSocket.js +5 -3
  12. package/es/components/material-exercise/exercise-file-editor/ExerciseFileEditor.d.ts +3 -5
  13. package/es/components/material-exercise/exercise-file-editor/ExerciseFileEditor.js +59 -21
  14. package/es/components/material-exercise/exercise-file-editor/ExerciseFileReadOnlyBanner.d.ts +5 -2
  15. package/es/components/material-exercise/exercise-file-editor/ExerciseFileReadOnlyBanner.js +1 -8
  16. package/es/components/material-exercise/exercise-file-editor/locales/en.json.js +1 -1
  17. package/es/components/material-exercise/exercise-file-editor/locales/ja.json.js +1 -1
  18. package/es/components/material-exercise/exercise-file-editor/locales/ko.json.js +1 -1
  19. package/es/components/material-exercise/exercise-file-editor/locales/th.json.js +1 -1
  20. package/es/components/material-exercise/exercise-runner/ExerciseRunnerControllerStatusMessage.js +5 -0
  21. package/es/hooks/useUsercodeEditWebSocket.d.ts +6 -1
  22. package/es/hooks/useUsercodeEditWebSocket.js +5 -3
  23. package/package.json +4 -4
@@ -1,6 +1,4 @@
1
1
  import React from 'react';
2
- /**
3
- * Exercise file (code) editor for editable files
4
- */
5
- declare const ExerciseFileEditor: React.VFC;
6
- export default ExerciseFileEditor;
2
+ import type { WithIntlComponentBuilderProps } from '@elice/intl';
3
+ declare const _default: React.ForwardRefExoticComponent<Omit<import("@elice/intl").IntlComponentExtraProps & Omit<WithIntlComponentBuilderProps, keyof import("@elice/intl").IntlComponentExtraProps | "__intl">, "ref"> & React.RefAttributes<any>>;
4
+ export default _default;
@@ -8,6 +8,7 @@ var React = require('react');
8
8
  var reactUse = require('react-use');
9
9
  var apiClient = require('@elice/api-client');
10
10
  var blocks = require('@elice/blocks');
11
+ var intl = require('@elice/intl');
11
12
  var types = require('@elice/types');
12
13
  var utils = require('@elice/utils');
13
14
  var recoil = require('recoil');
@@ -35,6 +36,8 @@ require('humps');
35
36
  require('ot-text-unicode');
36
37
  var useUsercodeEditWebSocket = require('../../../hooks/useUsercodeEditWebSocket.js');
37
38
  var ExerciseFileReadOnlyBanner = require('./ExerciseFileReadOnlyBanner.js');
39
+ var en = require('./locales/en.json.js');
40
+ var ko = require('./locales/ko.json.js');
38
41
 
39
42
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
40
43
 
@@ -46,7 +49,8 @@ var React__default = /*#__PURE__*/_interopDefaultCompat(React);
46
49
  /**
47
50
  * Exercise file (code) editor for editable files
48
51
  */
49
- var ExerciseFileEditor = function ExerciseFileEditor() {
52
+ var ExerciseFileEditor = function ExerciseFileEditor(_ref) {
53
+ var __intl = _ref.__intl;
50
54
  var _React$useContext = React__default.default.useContext(context.ExerciseContext),
51
55
  materialExerciseId = _React$useContext.materialExerciseId,
52
56
  exerciseRoomId = _React$useContext.exerciseRoomId,
@@ -104,7 +108,7 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
104
108
  * Set value of current file.
105
109
  */
106
110
  var setValue = /*#__PURE__*/function () {
107
- var _ref = _rollupPluginBabelHelpers.asyncToGenerator( /*#__PURE__*/_rollupPluginBabelHelpers.regeneratorRuntime().mark(function _callee(content) {
111
+ var _ref2 = _rollupPluginBabelHelpers.asyncToGenerator( /*#__PURE__*/_rollupPluginBabelHelpers.regeneratorRuntime().mark(function _callee(content) {
108
112
  var _a, readyExerciseImage;
109
113
  return _rollupPluginBabelHelpers.regeneratorRuntime().wrap(function _callee$(_context) {
110
114
  while (1) switch (_context.prev = _context.next) {
@@ -162,7 +166,7 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
162
166
  }, _callee);
163
167
  }));
164
168
  return function setValue(_x) {
165
- return _ref.apply(this, arguments);
169
+ return _ref2.apply(this, arguments);
166
170
  };
167
171
  }();
168
172
  /**
@@ -224,6 +228,9 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
224
228
  _iterator.f();
225
229
  }
226
230
  };
231
+ /**
232
+ * Handle `WRITE_SUCCEED` event.
233
+ */
227
234
  var handleWebsocketUsercodeWriteSucceed = function handleWebsocketUsercodeWriteSucceed(msg, requestId) {
228
235
  var request = usercodeSyncRequestsRef.current.get(requestId);
229
236
  if (request) {
@@ -231,6 +238,16 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
231
238
  request.onSucceed(msg);
232
239
  }
233
240
  };
241
+ /**
242
+ * Handle `WRITE_FAILED` event.
243
+ */
244
+ var handleWebsocketUsercodeWriteFailedAndReset = function handleWebsocketUsercodeWriteFailedAndReset(msg) {
245
+ blocks.Notification.error(__intl.formatMessage({
246
+ id: 'materialExercise.websocket.writeFailedAndReset'
247
+ }), {
248
+ duration: 5000
249
+ });
250
+ };
234
251
  /**
235
252
  * Usercode edit web socket.
236
253
  */
@@ -238,10 +255,16 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
238
255
  exerciseRoomId: exerciseRoomId,
239
256
  filename: activeFilename,
240
257
  onInit: function onInit() {
241
- return setWebSocketReady(true);
258
+ setWebSocketReady(false);
259
+ serverInitiatedVersions.current = [];
260
+ editorDocHistories.current = {};
261
+ setTimeout(function () {
262
+ return setWebSocketReady(true);
263
+ }, 100);
242
264
  },
243
265
  onWriteNoti: handleWebsocketUsercodeWriteNoti,
244
- onWriteSucceed: handleWebsocketUsercodeWriteSucceed
266
+ onWriteSucceed: handleWebsocketUsercodeWriteSucceed,
267
+ onWriteFailedAndReset: handleWebsocketUsercodeWriteFailedAndReset
245
268
  });
246
269
  /**
247
270
  *
@@ -312,7 +335,6 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
312
335
  // Handle editor content change.
313
336
  //
314
337
  React__default.default.useEffect(function () {
315
- var _a, _b;
316
338
  var _editorApis = editorApis.current;
317
339
  if (!_editorApis || !isReady) {
318
340
  return;
@@ -320,19 +342,24 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
320
342
  _editorApis.setValue(wsUsercode.doc);
321
343
  pushEditorDocHistories();
322
344
  var subscription = undefined;
323
- if (utils.FlutterApp.checkWebview()) {
324
- subscription = (_a = _editorApis.editor) === null || _a === void 0 ? void 0 : _a.onDidChangeModelContent(function (e) {
325
- pushEditorDocHistories();
326
- handleChange(e);
327
- utils.FlutterApp.onDidChangeContent();
328
- });
329
- } else {
330
- subscription = (_b = _editorApis.editor) === null || _b === void 0 ? void 0 : _b.onDidChangeModelContent(function (e) {
331
- pushEditorDocHistories();
332
- handleChange(e);
333
- });
334
- }
345
+ var __subscribe = function __subscribe() {
346
+ var _a, _b;
347
+ if (utils.FlutterApp.checkWebview()) {
348
+ subscription = (_a = _editorApis.editor) === null || _a === void 0 ? void 0 : _a.onDidChangeModelContent(function (e) {
349
+ pushEditorDocHistories();
350
+ handleChange(e);
351
+ utils.FlutterApp.onDidChangeContent();
352
+ });
353
+ } else {
354
+ subscription = (_b = _editorApis.editor) === null || _b === void 0 ? void 0 : _b.onDidChangeModelContent(function (e) {
355
+ pushEditorDocHistories();
356
+ handleChange(e);
357
+ });
358
+ }
359
+ };
360
+ var timeout = window.setTimeout(__subscribe, 10); // need to wait for editor to be ready.
335
361
  return function () {
362
+ window.clearTimeout(timeout);
336
363
  if (subscription) {
337
364
  subscription.dispose();
338
365
  }
@@ -437,7 +464,14 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
437
464
  column: true,
438
465
  width: "100%",
439
466
  height: "100%",
440
- children: [jsxRuntime.jsx(ExerciseFileReadOnlyBanner.default, {}), jsxRuntime.jsx(MonacoEditorLazy.default, {
467
+ style: {
468
+ // When the user focuses out and in again,
469
+ // Monaco editor can't restore focus well in Safari when parent's user-select is none.
470
+ WebkitUserSelect: !readOnly ? 'text' : undefined
471
+ },
472
+ children: [jsxRuntime.jsx(ExerciseFileReadOnlyBanner.default, {
473
+ __intl: __intl
474
+ }), jsxRuntime.jsx(MonacoEditorLazy.default, {
441
475
  defaultValue: "",
442
476
  filename: editorFilename,
443
477
  markers: wsUsercode.markers,
@@ -455,5 +489,9 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
455
489
  }, editorFilename)]
456
490
  });
457
491
  };
492
+ //
493
+ //
494
+ //
495
+ var ExerciseFileEditor$1 = new intl.IntlComponentBuilder(ExerciseFileEditor).add('ko', ko.default).add('en', en.default).addAsync('th', Promise.resolve().then(function () { return require('./locales/th.json.js'); })).addAsync('ja', Promise.resolve().then(function () { return require('./locales/ja.json.js'); })).build();
458
496
 
459
- exports.default = ExerciseFileEditor;
497
+ exports.default = ExerciseFileEditor$1;
@@ -1,4 +1,7 @@
1
1
  import React from 'react';
2
2
  import type { WithIntlComponentBuilderProps as EliceIntlProps } from '@elice/intl';
3
- declare const _default: React.ForwardRefExoticComponent<Omit<import("@elice/intl").IntlComponentExtraProps & Omit<EliceIntlProps, keyof import("@elice/intl").IntlComponentExtraProps | "__intl">, "ref"> & React.RefAttributes<any>>;
4
- export default _default;
3
+ /**
4
+ * Banner for readonly exercise file.
5
+ */
6
+ declare const ExerciseFileReadOnlyBanner: React.FC<EliceIntlProps>;
7
+ export default ExerciseFileReadOnlyBanner;
@@ -6,15 +6,12 @@ var jsxRuntime = require('react/jsx-runtime');
6
6
  var React = require('react');
7
7
  var blocks = require('@elice/blocks');
8
8
  var designTokens = require('@elice/design-tokens');
9
- var intl = require('@elice/intl');
10
9
  var styled = require('styled-components');
11
10
  require('../context/recoil.js');
12
11
  var context = require('../context/context.js');
13
12
  require('../context/recoilTypes.js');
14
13
  require('../context/subjects.js');
15
14
  require('../context/ExerciseProvider.js');
16
- var en = require('./locales/en.json.js');
17
- var ko = require('./locales/ko.json.js');
18
15
 
19
16
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
20
17
 
@@ -44,9 +41,5 @@ var ExerciseFileReadOnlyBanner = function ExerciseFileReadOnlyBanner(_ref) {
44
41
  })
45
42
  });
46
43
  };
47
- //
48
- //
49
- //
50
- var ExerciseFileReadOnlyBanner$1 = new intl.IntlComponentBuilder(ExerciseFileReadOnlyBanner).add('ko', ko.default).add('en', en.default).addAsync('th', Promise.resolve().then(function () { return require('./locales/th.json.js'); })).addAsync('ja', Promise.resolve().then(function () { return require('./locales/ja.json.js'); })).build();
51
44
 
52
- exports.default = ExerciseFileReadOnlyBanner$1;
45
+ exports.default = ExerciseFileReadOnlyBanner;
@@ -2,6 +2,6 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var messageEn = {"materialExercise.text.readOnlyBanner":"[Read-Only] This file is read-only."};
5
+ var messageEn = {"materialExercise.text.readOnlyBanner":"[Read-Only] This file is read-only.","materialExercise.websocket.writeFailedAndReset":"Failed to save the code and the content has been reset. Please try again shortly."};
6
6
 
7
7
  exports.default = messageEn;
@@ -2,6 +2,6 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var ja = {"materialExercise.text.readOnlyBanner":"[読み取り専用] このファイルは読み取り専用です。"};
5
+ var ja = {"materialExercise.text.readOnlyBanner":"[読み取り専用] このファイルは読み取り専用です。","materialExercise.websocket.writeFailedAndReset":"コードの保存に失敗し、内容が初期化されました。しばらくしてから再度お試しください。"};
6
6
 
7
7
  exports.default = ja;
@@ -2,6 +2,6 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var messageKo = {"materialExercise.text.readOnlyBanner":"[읽기전용] 이 파일은 읽기전용 입니다."};
5
+ var messageKo = {"materialExercise.text.readOnlyBanner":"[읽기전용] 이 파일은 읽기전용 입니다.","materialExercise.websocket.writeFailedAndReset":"코드 저장에 실패하여 내용이 초기화되었습니다. 잠시 후 다시 시도해주세요."};
6
6
 
7
7
  exports.default = messageKo;
@@ -2,6 +2,6 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var th = {"materialExercise.text.readOnlyBanner":"[อ่านเท่านั้น] ไฟล์นี้เป็นแบบอ่านเท่านั้น"};
5
+ var th = {"materialExercise.text.readOnlyBanner":"[อ่านเท่านั้น] ไฟล์นี้เป็นแบบอ่านเท่านั้น","materialExercise.websocket.writeFailedAndReset":"ไม่สามารถบันทึกรหัสได้ เนื้อหาได้ถูกรีเซ็ต กรุณาลองใหม่อีกครั้งในภายหลัง"};
6
6
 
7
7
  exports.default = th;
@@ -8,6 +8,7 @@ var React = require('react');
8
8
  var blocks = require('@elice/blocks');
9
9
  var intl = require('@elice/intl');
10
10
  var websocket = require('@elice/websocket');
11
+ var react = require('@emotion/react');
11
12
  var recoil = require('recoil');
12
13
  var styled = require('styled-components');
13
14
  var recoil$1 = require('../context/recoil.js');
@@ -25,6 +26,7 @@ var StyledMessageWrapper = styled__default.default(blocks.Flex).withConfig({
25
26
  componentId: "sc-1yqi8o2-0"
26
27
  })(["height:1rem;"]);
27
28
  var ExerciseRunnerControllerStatusMessage = function ExerciseRunnerControllerStatusMessage() {
29
+ var theme = react.useTheme();
28
30
  var intl$1 = intl.useRawEliceIntl();
29
31
  var websocketStatus = recoil.useRecoilValue(recoil$1.exerciseWebSocketTotalStatusQuery(['usercodeEdit', 'runnerRoom', 'stdio']));
30
32
  var _useState = React.useState(false),
@@ -66,6 +68,9 @@ var ExerciseRunnerControllerStatusMessage = function ExerciseRunnerControllerSta
66
68
  title: intl$1.formatMessage({
67
69
  id: 'exerciseRunner.controller.tooltip.closed'
68
70
  }),
71
+ overlayStyle: {
72
+ zIndex: theme.zIndex.tooltip
73
+ },
69
74
  children: jsxRuntime.jsxs(StyledMessageWrapper, {
70
75
  align: "center",
71
76
  children: [jsxRuntime.jsx(ExerciseRunnerControllerStatusIndicator.default, {
@@ -1,4 +1,4 @@
1
- import type { WebsocketUsercodeMessageWriteSucceed as WSUsercodeMessageWriteSucceed } from '@elice/types';
1
+ import type { WebsocketUsercodeMessageWriteFailed as WSUsercodeMessageWriteFailed, WebsocketUsercodeMessageWriteSucceed as WSUsercodeMessageWriteSucceed } from '@elice/types';
2
2
  import type { TextOp } from 'ot-text-unicode';
3
3
  interface CursorRange {
4
4
  from: number;
@@ -22,6 +22,10 @@ export type UseUsercodeEditWebSocketPropOnWriteNoti = (ots: TextOp, isFromServer
22
22
  * Event handler for `WRITE_SUCCEED` event.
23
23
  */
24
24
  export type UseUsercodeEditWebSocketPropOnWriteSucceed = (msg: WSUsercodeMessageWriteSucceed, requestId: number) => void;
25
+ /**
26
+ * Event handler for `WRITE_FAILED` event.
27
+ */
28
+ export type UseUsercodeEditWebSocketPropOnWriteFailedAndReset = (msg: WSUsercodeMessageWriteFailed) => void;
25
29
  /**
26
30
  * Props of `useUsercodeEditWebSocket`.
27
31
  */
@@ -31,6 +35,7 @@ interface UseUsercodeEditWebSocketProps {
31
35
  onInit: UseUsercodeEditWebSocketPropOnInit;
32
36
  onWriteNoti: UseUsercodeEditWebSocketPropOnWriteNoti;
33
37
  onWriteSucceed: UseUsercodeEditWebSocketPropOnWriteSucceed;
38
+ onWriteFailedAndReset: UseUsercodeEditWebSocketPropOnWriteFailedAndReset;
34
39
  }
35
40
  /**
36
41
  *
@@ -20,7 +20,7 @@ var randomWords__default = /*#__PURE__*/_interopDefaultCompat(randomWords);
20
20
  //
21
21
  //
22
22
  //
23
- var MAX_WRITE_FAIL_COUNT = 5;
23
+ var MAX_WRITE_FAIL_COUNT = 1; // FIXME: change retry count 5 -> 1 to avoid user confusion.
24
24
  /**
25
25
  *
26
26
  */
@@ -29,7 +29,8 @@ var useUsercodeEditWebSocket = function useUsercodeEditWebSocket(_ref) {
29
29
  filename = _ref.filename,
30
30
  onInit = _ref.onInit,
31
31
  onWriteNoti = _ref.onWriteNoti,
32
- onWriteSucceed = _ref.onWriteSucceed;
32
+ onWriteSucceed = _ref.onWriteSucceed,
33
+ onWriteFailedAndReset = _ref.onWriteFailedAndReset;
33
34
  var _return = React.useRef({});
34
35
  var _useMaterialConfig = materialSharedUtils.useMaterialConfig(),
35
36
  apiWsOriginUrl = _useMaterialConfig.apiWsOriginUrl;
@@ -342,7 +343,7 @@ var useUsercodeEditWebSocket = function useUsercodeEditWebSocket(_ref) {
342
343
  wsMessageUpdate(msg);
343
344
  break;
344
345
  case 'WRITE_FAILED':
345
- wsMessageWriteFailed();
346
+ wsMessageWriteFailed(msg);
346
347
  break;
347
348
  case 'WRITE_SUCCEED':
348
349
  wsMessageWriteSucceed(msg);
@@ -418,6 +419,7 @@ var useUsercodeEditWebSocket = function useUsercodeEditWebSocket(_ref) {
418
419
  writeFailCount.current += 1;
419
420
  if (MAX_WRITE_FAIL_COUNT < writeFailCount.current) {
420
421
  sendReset();
422
+ onWriteFailedAndReset(msg);
421
423
  } else {
422
424
  sendFetch();
423
425
  }
@@ -1,6 +1,4 @@
1
1
  import React from 'react';
2
- /**
3
- * Exercise file (code) editor for editable files
4
- */
5
- declare const ExerciseFileEditor: React.VFC;
6
- export default ExerciseFileEditor;
2
+ import type { WithIntlComponentBuilderProps } from '@elice/intl';
3
+ declare const _default: React.ForwardRefExoticComponent<Omit<import("@elice/intl").IntlComponentExtraProps & Omit<WithIntlComponentBuilderProps, keyof import("@elice/intl").IntlComponentExtraProps | "__intl">, "ref"> & React.RefAttributes<any>>;
4
+ export default _default;
@@ -3,7 +3,8 @@ import { jsx, jsxs } from 'react/jsx-runtime';
3
3
  import React, { useEffect } from 'react';
4
4
  import { useDebounce } from 'react-use';
5
5
  import { getOrgMaterialExerciseExerciseImageExerciseFileGet } from '@elice/api-client';
6
- import { Flex } from '@elice/blocks';
6
+ import { Flex, Notification } from '@elice/blocks';
7
+ import { IntlComponentBuilder } from '@elice/intl';
7
8
  import { enums } from '@elice/types';
8
9
  import { FlutterApp } from '@elice/utils';
9
10
  import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil';
@@ -31,6 +32,8 @@ import 'humps';
31
32
  import 'ot-text-unicode';
32
33
  import { useUsercodeEditWebSocket } from '../../../hooks/useUsercodeEditWebSocket.js';
33
34
  import ExerciseFileReadOnlyBanner from './ExerciseFileReadOnlyBanner.js';
35
+ import messageEn from './locales/en.json.js';
36
+ import messageKo from './locales/ko.json.js';
34
37
 
35
38
  //
36
39
  //
@@ -38,7 +41,8 @@ import ExerciseFileReadOnlyBanner from './ExerciseFileReadOnlyBanner.js';
38
41
  /**
39
42
  * Exercise file (code) editor for editable files
40
43
  */
41
- var ExerciseFileEditor = function ExerciseFileEditor() {
44
+ var ExerciseFileEditor = function ExerciseFileEditor(_ref) {
45
+ var __intl = _ref.__intl;
42
46
  var _React$useContext = React.useContext(ExerciseContext),
43
47
  materialExerciseId = _React$useContext.materialExerciseId,
44
48
  exerciseRoomId = _React$useContext.exerciseRoomId,
@@ -96,7 +100,7 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
96
100
  * Set value of current file.
97
101
  */
98
102
  var setValue = /*#__PURE__*/function () {
99
- var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(content) {
103
+ var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(content) {
100
104
  var _a, readyExerciseImage;
101
105
  return _regeneratorRuntime().wrap(function _callee$(_context) {
102
106
  while (1) switch (_context.prev = _context.next) {
@@ -154,7 +158,7 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
154
158
  }, _callee);
155
159
  }));
156
160
  return function setValue(_x) {
157
- return _ref.apply(this, arguments);
161
+ return _ref2.apply(this, arguments);
158
162
  };
159
163
  }();
160
164
  /**
@@ -216,6 +220,9 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
216
220
  _iterator.f();
217
221
  }
218
222
  };
223
+ /**
224
+ * Handle `WRITE_SUCCEED` event.
225
+ */
219
226
  var handleWebsocketUsercodeWriteSucceed = function handleWebsocketUsercodeWriteSucceed(msg, requestId) {
220
227
  var request = usercodeSyncRequestsRef.current.get(requestId);
221
228
  if (request) {
@@ -223,6 +230,16 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
223
230
  request.onSucceed(msg);
224
231
  }
225
232
  };
233
+ /**
234
+ * Handle `WRITE_FAILED` event.
235
+ */
236
+ var handleWebsocketUsercodeWriteFailedAndReset = function handleWebsocketUsercodeWriteFailedAndReset(msg) {
237
+ Notification.error(__intl.formatMessage({
238
+ id: 'materialExercise.websocket.writeFailedAndReset'
239
+ }), {
240
+ duration: 5000
241
+ });
242
+ };
226
243
  /**
227
244
  * Usercode edit web socket.
228
245
  */
@@ -230,10 +247,16 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
230
247
  exerciseRoomId: exerciseRoomId,
231
248
  filename: activeFilename,
232
249
  onInit: function onInit() {
233
- return setWebSocketReady(true);
250
+ setWebSocketReady(false);
251
+ serverInitiatedVersions.current = [];
252
+ editorDocHistories.current = {};
253
+ setTimeout(function () {
254
+ return setWebSocketReady(true);
255
+ }, 100);
234
256
  },
235
257
  onWriteNoti: handleWebsocketUsercodeWriteNoti,
236
- onWriteSucceed: handleWebsocketUsercodeWriteSucceed
258
+ onWriteSucceed: handleWebsocketUsercodeWriteSucceed,
259
+ onWriteFailedAndReset: handleWebsocketUsercodeWriteFailedAndReset
237
260
  });
238
261
  /**
239
262
  *
@@ -304,7 +327,6 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
304
327
  // Handle editor content change.
305
328
  //
306
329
  React.useEffect(function () {
307
- var _a, _b;
308
330
  var _editorApis = editorApis.current;
309
331
  if (!_editorApis || !isReady) {
310
332
  return;
@@ -312,19 +334,24 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
312
334
  _editorApis.setValue(wsUsercode.doc);
313
335
  pushEditorDocHistories();
314
336
  var subscription = undefined;
315
- if (FlutterApp.checkWebview()) {
316
- subscription = (_a = _editorApis.editor) === null || _a === void 0 ? void 0 : _a.onDidChangeModelContent(function (e) {
317
- pushEditorDocHistories();
318
- handleChange(e);
319
- FlutterApp.onDidChangeContent();
320
- });
321
- } else {
322
- subscription = (_b = _editorApis.editor) === null || _b === void 0 ? void 0 : _b.onDidChangeModelContent(function (e) {
323
- pushEditorDocHistories();
324
- handleChange(e);
325
- });
326
- }
337
+ var __subscribe = function __subscribe() {
338
+ var _a, _b;
339
+ if (FlutterApp.checkWebview()) {
340
+ subscription = (_a = _editorApis.editor) === null || _a === void 0 ? void 0 : _a.onDidChangeModelContent(function (e) {
341
+ pushEditorDocHistories();
342
+ handleChange(e);
343
+ FlutterApp.onDidChangeContent();
344
+ });
345
+ } else {
346
+ subscription = (_b = _editorApis.editor) === null || _b === void 0 ? void 0 : _b.onDidChangeModelContent(function (e) {
347
+ pushEditorDocHistories();
348
+ handleChange(e);
349
+ });
350
+ }
351
+ };
352
+ var timeout = window.setTimeout(__subscribe, 10); // need to wait for editor to be ready.
327
353
  return function () {
354
+ window.clearTimeout(timeout);
328
355
  if (subscription) {
329
356
  subscription.dispose();
330
357
  }
@@ -429,7 +456,14 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
429
456
  column: true,
430
457
  width: "100%",
431
458
  height: "100%",
432
- children: [jsx(ExerciseFileReadOnlyBanner, {}), jsx(MonacoEditorLazy, {
459
+ style: {
460
+ // When the user focuses out and in again,
461
+ // Monaco editor can't restore focus well in Safari when parent's user-select is none.
462
+ WebkitUserSelect: !readOnly ? 'text' : undefined
463
+ },
464
+ children: [jsx(ExerciseFileReadOnlyBanner, {
465
+ __intl: __intl
466
+ }), jsx(MonacoEditorLazy, {
433
467
  defaultValue: "",
434
468
  filename: editorFilename,
435
469
  markers: wsUsercode.markers,
@@ -447,5 +481,9 @@ var ExerciseFileEditor = function ExerciseFileEditor() {
447
481
  }, editorFilename)]
448
482
  });
449
483
  };
484
+ //
485
+ //
486
+ //
487
+ var ExerciseFileEditor$1 = new IntlComponentBuilder(ExerciseFileEditor).add('ko', messageKo).add('en', messageEn).addAsync('th', import('./locales/th.json.js')).addAsync('ja', import('./locales/ja.json.js')).build();
450
488
 
451
- export { ExerciseFileEditor as default };
489
+ export { ExerciseFileEditor$1 as default };
@@ -1,4 +1,7 @@
1
1
  import React from 'react';
2
2
  import type { WithIntlComponentBuilderProps as EliceIntlProps } from '@elice/intl';
3
- declare const _default: React.ForwardRefExoticComponent<Omit<import("@elice/intl").IntlComponentExtraProps & Omit<EliceIntlProps, keyof import("@elice/intl").IntlComponentExtraProps | "__intl">, "ref"> & React.RefAttributes<any>>;
4
- export default _default;
3
+ /**
4
+ * Banner for readonly exercise file.
5
+ */
6
+ declare const ExerciseFileReadOnlyBanner: React.FC<EliceIntlProps>;
7
+ export default ExerciseFileReadOnlyBanner;
@@ -2,15 +2,12 @@ import { jsx } from 'react/jsx-runtime';
2
2
  import React from 'react';
3
3
  import { Text } from '@elice/blocks';
4
4
  import { base } from '@elice/design-tokens';
5
- import { IntlComponentBuilder } from '@elice/intl';
6
5
  import styled from 'styled-components';
7
6
  import '../context/recoil.js';
8
7
  import { ExerciseContext } from '../context/context.js';
9
8
  import '../context/recoilTypes.js';
10
9
  import '../context/subjects.js';
11
10
  import '../context/ExerciseProvider.js';
12
- import messageEn from './locales/en.json.js';
13
- import messageKo from './locales/ko.json.js';
14
11
 
15
12
  var StyledEditorReadOnlyBanner = styled.div.withConfig({
16
13
  componentId: "sc-kh9iw2-0"
@@ -35,9 +32,5 @@ var ExerciseFileReadOnlyBanner = function ExerciseFileReadOnlyBanner(_ref) {
35
32
  })
36
33
  });
37
34
  };
38
- //
39
- //
40
- //
41
- var ExerciseFileReadOnlyBanner$1 = new IntlComponentBuilder(ExerciseFileReadOnlyBanner).add('ko', messageKo).add('en', messageEn).addAsync('th', import('./locales/th.json.js')).addAsync('ja', import('./locales/ja.json.js')).build();
42
35
 
43
- export { ExerciseFileReadOnlyBanner$1 as default };
36
+ export { ExerciseFileReadOnlyBanner as default };
@@ -1,3 +1,3 @@
1
- var messageEn = {"materialExercise.text.readOnlyBanner":"[Read-Only] This file is read-only."};
1
+ var messageEn = {"materialExercise.text.readOnlyBanner":"[Read-Only] This file is read-only.","materialExercise.websocket.writeFailedAndReset":"Failed to save the code and the content has been reset. Please try again shortly."};
2
2
 
3
3
  export { messageEn as default };
@@ -1,3 +1,3 @@
1
- var ja = {"materialExercise.text.readOnlyBanner":"[読み取り専用] このファイルは読み取り専用です。"};
1
+ var ja = {"materialExercise.text.readOnlyBanner":"[読み取り専用] このファイルは読み取り専用です。","materialExercise.websocket.writeFailedAndReset":"コードの保存に失敗し、内容が初期化されました。しばらくしてから再度お試しください。"};
2
2
 
3
3
  export { ja as default };
@@ -1,3 +1,3 @@
1
- var messageKo = {"materialExercise.text.readOnlyBanner":"[읽기전용] 이 파일은 읽기전용 입니다."};
1
+ var messageKo = {"materialExercise.text.readOnlyBanner":"[읽기전용] 이 파일은 읽기전용 입니다.","materialExercise.websocket.writeFailedAndReset":"코드 저장에 실패하여 내용이 초기화되었습니다. 잠시 후 다시 시도해주세요."};
2
2
 
3
3
  export { messageKo as default };
@@ -1,3 +1,3 @@
1
- var th = {"materialExercise.text.readOnlyBanner":"[อ่านเท่านั้น] ไฟล์นี้เป็นแบบอ่านเท่านั้น"};
1
+ var th = {"materialExercise.text.readOnlyBanner":"[อ่านเท่านั้น] ไฟล์นี้เป็นแบบอ่านเท่านั้น","materialExercise.websocket.writeFailedAndReset":"ไม่สามารถบันทึกรหัสได้ เนื้อหาได้ถูกรีเซ็ต กรุณาลองใหม่อีกครั้งในภายหลัง"};
2
2
 
3
3
  export { th as default };
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
4
4
  import { Flex, Tooltip, Text } from '@elice/blocks';
5
5
  import { useRawEliceIntl } from '@elice/intl';
6
6
  import { EliceWebSocket } from '@elice/websocket';
7
+ import { useTheme } from '@emotion/react';
7
8
  import { useRecoilValue } from 'recoil';
8
9
  import styled from 'styled-components';
9
10
  import { exerciseWebSocketTotalStatusQuery } from '../context/recoil.js';
@@ -17,6 +18,7 @@ var StyledMessageWrapper = styled(Flex).withConfig({
17
18
  componentId: "sc-1yqi8o2-0"
18
19
  })(["height:1rem;"]);
19
20
  var ExerciseRunnerControllerStatusMessage = function ExerciseRunnerControllerStatusMessage() {
21
+ var theme = useTheme();
20
22
  var intl = useRawEliceIntl();
21
23
  var websocketStatus = useRecoilValue(exerciseWebSocketTotalStatusQuery(['usercodeEdit', 'runnerRoom', 'stdio']));
22
24
  var _useState = useState(false),
@@ -58,6 +60,9 @@ var ExerciseRunnerControllerStatusMessage = function ExerciseRunnerControllerSta
58
60
  title: intl.formatMessage({
59
61
  id: 'exerciseRunner.controller.tooltip.closed'
60
62
  }),
63
+ overlayStyle: {
64
+ zIndex: theme.zIndex.tooltip
65
+ },
61
66
  children: jsxs(StyledMessageWrapper, {
62
67
  align: "center",
63
68
  children: [jsx(ExerciseRunnerControllerStatusIndicator, {
@@ -1,4 +1,4 @@
1
- import type { WebsocketUsercodeMessageWriteSucceed as WSUsercodeMessageWriteSucceed } from '@elice/types';
1
+ import type { WebsocketUsercodeMessageWriteFailed as WSUsercodeMessageWriteFailed, WebsocketUsercodeMessageWriteSucceed as WSUsercodeMessageWriteSucceed } from '@elice/types';
2
2
  import type { TextOp } from 'ot-text-unicode';
3
3
  interface CursorRange {
4
4
  from: number;
@@ -22,6 +22,10 @@ export type UseUsercodeEditWebSocketPropOnWriteNoti = (ots: TextOp, isFromServer
22
22
  * Event handler for `WRITE_SUCCEED` event.
23
23
  */
24
24
  export type UseUsercodeEditWebSocketPropOnWriteSucceed = (msg: WSUsercodeMessageWriteSucceed, requestId: number) => void;
25
+ /**
26
+ * Event handler for `WRITE_FAILED` event.
27
+ */
28
+ export type UseUsercodeEditWebSocketPropOnWriteFailedAndReset = (msg: WSUsercodeMessageWriteFailed) => void;
25
29
  /**
26
30
  * Props of `useUsercodeEditWebSocket`.
27
31
  */
@@ -31,6 +35,7 @@ interface UseUsercodeEditWebSocketProps {
31
35
  onInit: UseUsercodeEditWebSocketPropOnInit;
32
36
  onWriteNoti: UseUsercodeEditWebSocketPropOnWriteNoti;
33
37
  onWriteSucceed: UseUsercodeEditWebSocketPropOnWriteSucceed;
38
+ onWriteFailedAndReset: UseUsercodeEditWebSocketPropOnWriteFailedAndReset;
34
39
  }
35
40
  /**
36
41
  *
@@ -13,7 +13,7 @@ import { uniCount, uniToStrPos } from 'unicount';
13
13
  //
14
14
  //
15
15
  //
16
- var MAX_WRITE_FAIL_COUNT = 5;
16
+ var MAX_WRITE_FAIL_COUNT = 1; // FIXME: change retry count 5 -> 1 to avoid user confusion.
17
17
  /**
18
18
  *
19
19
  */
@@ -22,7 +22,8 @@ var useUsercodeEditWebSocket = function useUsercodeEditWebSocket(_ref) {
22
22
  filename = _ref.filename,
23
23
  onInit = _ref.onInit,
24
24
  onWriteNoti = _ref.onWriteNoti,
25
- onWriteSucceed = _ref.onWriteSucceed;
25
+ onWriteSucceed = _ref.onWriteSucceed,
26
+ onWriteFailedAndReset = _ref.onWriteFailedAndReset;
26
27
  var _return = useRef({});
27
28
  var _useMaterialConfig = useMaterialConfig(),
28
29
  apiWsOriginUrl = _useMaterialConfig.apiWsOriginUrl;
@@ -335,7 +336,7 @@ var useUsercodeEditWebSocket = function useUsercodeEditWebSocket(_ref) {
335
336
  wsMessageUpdate(msg);
336
337
  break;
337
338
  case 'WRITE_FAILED':
338
- wsMessageWriteFailed();
339
+ wsMessageWriteFailed(msg);
339
340
  break;
340
341
  case 'WRITE_SUCCEED':
341
342
  wsMessageWriteSucceed(msg);
@@ -411,6 +412,7 @@ var useUsercodeEditWebSocket = function useUsercodeEditWebSocket(_ref) {
411
412
  writeFailCount.current += 1;
412
413
  if (MAX_WRITE_FAIL_COUNT < writeFailCount.current) {
413
414
  sendReset();
415
+ onWriteFailedAndReset(msg);
414
416
  } else {
415
417
  sendFetch();
416
418
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elice/material-exercise",
3
- "version": "1.250618.0",
3
+ "version": "1.250711.0",
4
4
  "description": "User view and editing components of Elice material exercise",
5
5
  "repository": "https://git.elicer.io/elice/frontend/library/elice-material",
6
6
  "license": "UNLICENSED",
@@ -77,14 +77,14 @@
77
77
  "xterm-addon-fit": "^0.5.0"
78
78
  },
79
79
  "devDependencies": {
80
- "@elice/api-client": "^1.241220.0",
80
+ "@elice/api-client": "^1.250702.0",
81
81
  "@elice/blocks": "1.241007.0",
82
82
  "@elice/design-tokens": "^1.220803.0",
83
83
  "@elice/icons": "^1.230814.0",
84
84
  "@elice/intl": "0.241127.0",
85
85
  "@elice/markdown": "1.241015.0",
86
- "@elice/material-shared-types": "1.250618.0",
87
- "@elice/material-shared-utils": "1.250618.0",
86
+ "@elice/material-shared-types": "1.250711.0",
87
+ "@elice/material-shared-utils": "1.250711.0",
88
88
  "@elice/mui-elements": "^5.250108.0-controllabel.0",
89
89
  "@elice/mui-system": "^5.250108.0-controllabel.0",
90
90
  "@elice/types": "^1.250612.0-teamleaderboard.1",