From f8ad7eb82d45fe83a89472f94fe3698543a59ee5 Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Mon, 16 Dec 2024 12:28:55 +0100 Subject: [PATCH 1/7] Docs: Add documentation for Redirect action and update .gitignore (#1422) * chore: add docs-pages-build to .gitignore * docs: add docstring for Redirect action class * refactor: Remove attributes from redirect action's docstring --- .gitignore | 2 ++ backend/experiment/actions/redirect.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ae6c06af5..048b61d64 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ e2e/screenshots e2e/tests/__pycache__ temp/ + +docs-pages-build/ diff --git a/backend/experiment/actions/redirect.py b/backend/experiment/actions/redirect.py index ec0d050a6..15e05565c 100644 --- a/backend/experiment/actions/redirect.py +++ b/backend/experiment/actions/redirect.py @@ -2,7 +2,19 @@ class Redirect(BaseAction): - ID = 'REDIRECT' + """ + Redirect Action + + This action is used to redirect the user to a specified URL. + + Args: + url (str): The URL to redirect to. + + Example: + redirect_action = Redirect('https://example.com') + """ + + ID = "REDIRECT" def __init__(self, url): self.url = url From dc3ca4913c0d9632dfa6d1b763e4e3ec3c6157c1 Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Mon, 16 Dec 2024 12:34:26 +0100 Subject: [PATCH 2/7] Docs: Document HTML action class (#1423) * refactor: improve docstring for HTML action class and its initializer * refactor: enhance docstring for HTML action class and its initializer * chore: add docs-pages-build to .gitignore * refactor: Remove attributes from html action's docstring * chore: Add a bit more context to html action docstring --- backend/experiment/actions/html.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/backend/experiment/actions/html.py b/backend/experiment/actions/html.py index bc5052311..bf69524ec 100644 --- a/backend/experiment/actions/html.py +++ b/backend/experiment/actions/html.py @@ -2,16 +2,19 @@ from .base_action import BaseAction -class HTML(BaseAction): # pylint: disable=too-few-public-methods - """ - A custom view that handles a custom HTML question - Relates to client component: HTML.js +class HTML(BaseAction): + """An action that renders HTML content. See also the `HTML.tsx` component in the frontend project. + + Args: + body (str): The HTML body content + + Examples: + To render a simple HTML snippet with a title and a paragraph: + + >>> html_action = HTML('

Hello, world!

This is a simple HTML snippet.

') """ - ID = 'HTML' + ID = "HTML" - def __init__(self, body): - """ - - body: HTML body - """ + def __init__(self, body: str): self.body = body From 4688aaba2cda8f0a49e4d222840f0daa61f46a8d Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Mon, 16 Dec 2024 13:50:59 +0100 Subject: [PATCH 3/7] Added: Enhance MatchingPairs game with tutorial overlay (#1416) * feat: Allow Button to be clicked more than once if the `clickOnce` prop is set to `false` (`true` by default) * feat: Add Overlay component with customizable content and close functionality * feat: Integrate MSW for API mocking in Storybook and update MatchingPairs story to use handlers * feat: Add tutorial messages to MatchingPairs game and integrate tutorial overlay in UI * style: Decrease overlay's background color's opacity to increase visibility of game behind it. * style: Adjust Overlay component width and body margin for improved layout * test: Add unit tests for Overlay component functionality and rendering * test: Add tutorial overlay integration tests for MatchingPairs component * test: Enable previously skipped tests for MatchingPairs component and refactor state initialization * refactor: Remove in-between turns display from MatchingPairs component * fix: Fix checking if board is empty * style: Update Overlay component styles and button text for improved visibility and clarity (dark opaque background with white text instead of a white dialog with black text) * fix: Correct condition for checking 'seen' status in MatchingPairsGame scoring logic * style: Update transition effects in Overlay component for smoother animations * refactor: Turn off tutorial in matching pairs lite game * lint: Update string quotes for consistency in MatchingPairsICMPC class and remove unused import * feat: Add MatchingPairs2025 class with tutorial messages and docstrings * fix: Explicitly set tutorial to None in MatchingPairsGame class * feat: Add overlay visibility tracking to MatchingPairs game logic * fix: Remove unused overlay visibility tracking in MatchingPairsGame class as the information was already being stored and this was causing an error * test: Add test for "overlay_was_shown" value in a result's json data --- backend/experiment/actions/playback.py | 87 +-- backend/experiment/rules/__init__.py | 2 + backend/experiment/rules/matching_pairs.py | 8 +- .../experiment/rules/matching_pairs_2025.py | 28 + .../experiment/rules/matching_pairs_icmpc.py | 5 +- .../experiment/rules/matching_pairs_lite.py | 27 +- .../rules/tests/test_matching_pairs.py | 61 +-- frontend/.pnp.cjs | 501 ++++++++++++++++++ frontend/.storybook/preview.js | 6 + frontend/package.json | 7 + frontend/public/mockServiceWorker.js | 295 +++++++++++ frontend/src/components/Button/Button.tsx | 21 +- .../MatchingPairs/MatchingPairs.test.tsx | 255 ++++++++- .../MatchingPairs/MatchingPairs.tsx | 239 ++++++--- frontend/src/components/Overlay/Overlay.scss | 80 +++ .../src/components/Overlay/Overlay.test.tsx | 119 +++++ frontend/src/components/Overlay/Overlay.tsx | 78 +++ frontend/src/components/Playback/Playback.tsx | 3 +- ....stories.jsx => MatchingPairs.stories.tsx} | 39 +- frontend/src/stories/Overlay.stories.tsx | 80 +++ frontend/src/types/Playback.ts | 1 + frontend/yarn.lock | 410 +++++++++++++- 22 files changed, 2135 insertions(+), 217 deletions(-) create mode 100644 backend/experiment/rules/matching_pairs_2025.py create mode 100644 frontend/public/mockServiceWorker.js create mode 100644 frontend/src/components/Overlay/Overlay.scss create mode 100644 frontend/src/components/Overlay/Overlay.test.tsx create mode 100644 frontend/src/components/Overlay/Overlay.tsx rename frontend/src/stories/{MatchingPairs.stories.jsx => MatchingPairs.stories.tsx} (87%) create mode 100644 frontend/src/stories/Overlay.stories.tsx diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index 4370c0ace..0791eb964 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -5,47 +5,47 @@ from section.validators import audio_extensions # player types -TYPE_AUTOPLAY = 'AUTOPLAY' -TYPE_BUTTON = 'BUTTON' -TYPE_IMAGE = 'IMAGE' -TYPE_MULTIPLAYER = 'MULTIPLAYER' -TYPE_MATCHINGPAIRS = 'MATCHINGPAIRS' +TYPE_AUTOPLAY = "AUTOPLAY" +TYPE_BUTTON = "BUTTON" +TYPE_IMAGE = "IMAGE" +TYPE_MULTIPLAYER = "MULTIPLAYER" +TYPE_MATCHINGPAIRS = "MATCHINGPAIRS" # playback methods -PLAY_EXTERNAL = 'EXTERNAL' -PLAY_HTML = 'HTML' -PLAY_BUFFER = 'BUFFER' -PLAY_NOAUDIO = 'NOAUDIO' +PLAY_EXTERNAL = "EXTERNAL" +PLAY_HTML = "HTML" +PLAY_BUFFER = "BUFFER" +PLAY_NOAUDIO = "NOAUDIO" class Playback(BaseAction): - ''' A playback base class for different kinds of players - - sections: a list of sections (in many cases, will only contain *one* section) - - preload_message: text to display during preload - - instruction: text to display during presentation of the sound - - play_from: where in the audio file to start playing/ - - show_animation: whether to show animations with this player - - mute: whether to mute the audio - - timeout_after_playback: once playback has finished, add optional timeout (in seconds) before proceeding - - stop_audio_after: stop playback after so many seconds - - resume_play: if the playback should resume from where a previous view left off - ''' + """A playback base class for different kinds of players + - sections: a list of sections (in many cases, will only contain *one* section) + - preload_message: text to display during preload + - instruction: text to display during presentation of the sound + - play_from: where in the audio file to start playing/ + - show_animation: whether to show animations with this player + - mute: whether to mute the audio + - timeout_after_playback: once playback has finished, add optional timeout (in seconds) before proceeding + - stop_audio_after: stop playback after so many seconds + - resume_play: if the playback should resume from where a previous view left off + """ def __init__( self, sections, - preload_message='', - instruction='', + preload_message="", + instruction="", play_from=0, show_animation=False, mute=False, timeout_after_playback=None, stop_audio_after=None, resume_play=False, - style=FrontendStyle() + style=FrontendStyle(), + tutorial: Dict | None = None, ): - self.sections = [{'id': s.id, 'url': s.absolute_url(), 'group': s.group} - for s in sections] + self.sections = [{"id": s.id, "url": s.absolute_url(), "group": s.group} for s in sections] self.play_method = determine_play_method(sections[0]) self.show_animation = show_animation self.preload_message = preload_message @@ -56,13 +56,14 @@ def __init__( self.stop_audio_after = stop_audio_after self.resume_play = resume_play self.style = style + self.tutorial = tutorial class Autoplay(Playback): - ''' + """ This player starts playing automatically - show_animation: if True, show a countdown and moving histogram - ''' + """ def __init__(self, sections, **kwargs): super().__init__(sections, **kwargs) @@ -70,10 +71,10 @@ def __init__(self, sections, **kwargs): class PlayButton(Playback): - ''' + """ This player shows a button, which triggers playback - play_once: if True, button will be disabled after one play - ''' + """ def __init__(self, sections, play_once=False, **kwargs): super().__init__(sections, **kwargs) @@ -82,11 +83,11 @@ def __init__(self, sections, play_once=False, **kwargs): class Multiplayer(PlayButton): - ''' + """ This is a player with multiple play buttons - stop_audio_after: after how many seconds to stop audio - labels: pass list of strings if players should have custom labels - ''' + """ def __init__( self, @@ -102,50 +103,50 @@ def __init__( self.style = style if labels: if len(labels) != len(self.sections): - raise UserWarning( - 'Number of labels and sections for the play buttons do not match') + raise UserWarning("Number of labels and sections for the play buttons do not match") self.labels = labels class ImagePlayer(Multiplayer): - ''' + """ This is a special case of the Multiplayer: it shows an image next to each play button - ''' + """ def __init__(self, sections, images, image_labels=[], **kwargs): super().__init__(sections, **kwargs) self.ID = TYPE_IMAGE if len(images) != len(self.sections): - raise UserWarning( - 'Number of images and sections for the ImagePlayer do not match') + raise UserWarning("Number of images and sections for the ImagePlayer do not match") self.images = images if image_labels: if len(image_labels) != len(self.sections): - raise UserWarning( - 'Number of image labels and sections do not match') + raise UserWarning("Number of image labels and sections do not match") self.image_labels = image_labels class MatchingPairs(Multiplayer): - ''' + """ This is a special case of multiplayer: play buttons are represented as cards - sections: a list of sections (in many cases, will only contain *one* section) - score_feedback_display: how to display the score feedback (large-top, small-bottom-right, hidden) - ''' + """ - def __init__(self, sections: List[Dict], score_feedback_display: str = 'large-top', **kwargs): + def __init__( + self, sections: List[Dict], score_feedback_display: str = "large-top", tutorial: Dict | None = None, **kwargs + ): super().__init__(sections, **kwargs) self.ID = TYPE_MATCHINGPAIRS self.score_feedback_display = score_feedback_display + self.tutorial = tutorial def determine_play_method(section): filename = str(section.filename) if not is_audio_file(filename): return PLAY_NOAUDIO - elif filename.startswith('http'): + elif filename.startswith("http"): return PLAY_EXTERNAL elif section.duration > 45: return PLAY_HTML diff --git a/backend/experiment/rules/__init__.py b/backend/experiment/rules/__init__.py index 7925417d2..a0f68e155 100644 --- a/backend/experiment/rules/__init__.py +++ b/backend/experiment/rules/__init__.py @@ -15,6 +15,7 @@ from .matching_pairs_fixed import MatchingPairsFixed from .matching_pairs_lite import MatchingPairsLite from .matching_pairs_icmpc import MatchingPairsICMPC +from .matching_pairs_2025 import MatchingPairs2025 from .musical_preferences import MusicalPreferences from .rhythm_battery_final import RhythmBatteryFinal from .rhythm_battery_intro import RhythmBatteryIntro @@ -59,6 +60,7 @@ MatchingPairsGame.ID: MatchingPairsGame, MatchingPairsLite.ID: MatchingPairsLite, MatchingPairsICMPC.ID: MatchingPairsICMPC, + MatchingPairs2025.ID: MatchingPairs2025, MusicalPreferences.ID: MusicalPreferences, RhythmBatteryFinal.ID: RhythmBatteryFinal, RhythmBatteryIntro.ID: RhythmBatteryIntro, diff --git a/backend/experiment/rules/matching_pairs.py b/backend/experiment/rules/matching_pairs.py index 208fe1086..e4cde2739 100644 --- a/backend/experiment/rules/matching_pairs.py +++ b/backend/experiment/rules/matching_pairs.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _ from .base import Base -from experiment.actions import Consent, Explainer, Final, Playlist, Step, Trial +from experiment.actions import Explainer, Final, Playlist, Step, Trial from experiment.actions.playback import MatchingPairs from result.utils import prepare_result @@ -17,6 +17,7 @@ class MatchingPairsGame(Base): num_pairs = 8 show_animation = True score_feedback_display = "large-top" + tutorial = None contact_email = "aml.tunetwins@gmail.com" random_seed = None @@ -121,6 +122,7 @@ def get_matching_pairs_trial(self, session): stop_audio_after=5, show_animation=self.show_animation, score_feedback_display=self.score_feedback_display, + tutorial=self.tutorial, ) trial = Trial(title="Tune twins", playback=playback, feedback_form=None, config={"show_continue_button": False}) return trial @@ -139,14 +141,14 @@ def calculate_intermediate_score(self, session, result): second_section = Section.objects.get(pk=second_card["id"]) second_card["filename"] = str(second_section.filename) if first_section.group == second_section.group: - if "seen" in second_card: + if "seen" in second_card and second_card["seen"]: score = 20 given_response = "match" else: score = 10 given_response = "lucky match" else: - if "seen" in second_card: + if "seen" in second_card and second_card["seen"]: score = -10 given_response = "misremembered" else: diff --git a/backend/experiment/rules/matching_pairs_2025.py b/backend/experiment/rules/matching_pairs_2025.py new file mode 100644 index 000000000..71f8327ea --- /dev/null +++ b/backend/experiment/rules/matching_pairs_2025.py @@ -0,0 +1,28 @@ +from django.utils.translation import gettext_lazy as _ + +from .matching_pairs import MatchingPairsGame + + +class MatchingPairs2025(MatchingPairsGame): + """This is the working version of the Matching Pairs game for the 2025 Tunetwins experiment. The difference between this version and the original Matching Pairs game is that this version has some additional tutorial messages. These messages are intended to help the user understand the game. The tutorial messages are as follows: + - no_match: This was not a match, so you get 0 points. Please try again to see if you can find a matching pair. + - lucky_match: You got a matching pair, but you didn't hear both cards before. This is considered a lucky match. You get 10 points. + - memory_match: You got a matching pair. You get 20 points. + - misremembered: You thought you found a matching pair, but you didn't. This is considered a misremembered pair. You lose 10 points. + + The tutorial messages are displayed to the user in an overlay on the game screen. + """ + + ID = "MATCHING_PAIRS_2025" + tutorial = { + "no_match": _( + "This was not a match, so you get 0 points. Please try again to see if you can find a matching pair." + ), + "lucky_match": _( + "You got a matching pair, but you didn't hear both cards before. This is considered a lucky match. You get 10 points." + ), + "memory_match": _("You got a matching pair. You get 20 points."), + "misremembered": _( + "You thought you found a matching pair, but you didn't. This is considered a misremembered pair. You lose 10 points." + ), + } diff --git a/backend/experiment/rules/matching_pairs_icmpc.py b/backend/experiment/rules/matching_pairs_icmpc.py index d61dd4a7a..5ecf54f77 100644 --- a/backend/experiment/rules/matching_pairs_icmpc.py +++ b/backend/experiment/rules/matching_pairs_icmpc.py @@ -1,12 +1,11 @@ from django.utils.translation import gettext_lazy as _ from .matching_pairs import MatchingPairsGame -from experiment.actions.form import TextQuestion class MatchingPairsICMPC(MatchingPairsGame): - ID = 'MATCHING_PAIRS_ICMPC' + ID = "MATCHING_PAIRS_ICMPC" def __init__(self): super().__init__() - self.question_series[0]['keys'].append('fame_name') + self.question_series[0]["keys"].append("fame_name") diff --git a/backend/experiment/rules/matching_pairs_lite.py b/backend/experiment/rules/matching_pairs_lite.py index aba5d5ab7..4dde14fcf 100644 --- a/backend/experiment/rules/matching_pairs_lite.py +++ b/backend/experiment/rules/matching_pairs_lite.py @@ -8,33 +8,28 @@ class MatchingPairsLite(MatchingPairsGame): - ID = 'MATCHING_PAIRS_LITE' + ID = "MATCHING_PAIRS_LITE" num_pairs = 8 show_animation = False - score_feedback_display = 'small-bottom-right' - contact_email = 'aml.tunetwins@gmail.com' + score_feedback_display = "small-bottom-right" + contact_email = "aml.tunetwins@gmail.com" def next_round(self, session): playlist = Playlist(session.block.playlists.all()) - info = Info('', - heading='Press start to enter the game', - button_label='Start') + info = Info("", heading="Press start to enter the game", button_label="Start") if session.get_rounds_passed() < 1: trial = self.get_matching_pairs_trial(session) return [playlist, info, trial] else: - return final_action_with_optional_button(session, final_text='End of the game', title='Score', button_text='Back to dashboard') + return final_action_with_optional_button( + session, final_text="End of the game", title="Score", button_text="Back to dashboard" + ) def select_sections(self, session): - pairs = list(session.playlist.section_set.order_by().distinct( - 'group').values_list('group', flat=True)) - selected_pairs = pairs[:self.num_pairs] - originals = session.playlist.section_set.filter( - group__in=selected_pairs, tag='Original' - ) - degradations = session.playlist.section_set.exclude(tag='Original').filter( - group__in=selected_pairs - ) + pairs = list(session.playlist.section_set.order_by().distinct("group").values_list("group", flat=True)) + selected_pairs = pairs[: self.num_pairs] + originals = session.playlist.section_set.filter(group__in=selected_pairs, tag="Original") + degradations = session.playlist.section_set.exclude(tag="Original").filter(group__in=selected_pairs) if degradations: sections = list(originals) + list(degradations) return self.shuffle_sections(sections) diff --git a/backend/experiment/rules/tests/test_matching_pairs.py b/backend/experiment/rules/tests/test_matching_pairs.py index 84729a9cc..d4aa025b5 100644 --- a/backend/experiment/rules/tests/test_matching_pairs.py +++ b/backend/experiment/rules/tests/test_matching_pairs.py @@ -11,7 +11,6 @@ class MatchingPairsTest(TestCase): - @classmethod def setUpTestData(cls): create_default_questions() @@ -32,16 +31,12 @@ def setUpTestData(cls): "default,TwinPeaks_0_E1,0.0,10.0,MatchingPairs/2ndDegradation/TwinPeaks_0_E1.mp3,2ndDegradation,86\n" "default,TwinPeaks_1_E1,0.0,10.0,MatchingPairs/Original/TwinPeaks_1_E1.mp3,Original,86\n" ) - cls.playlist = Playlist.objects.create(name='TestMatchingPairs') + cls.playlist = Playlist.objects.create(name="TestMatchingPairs") cls.playlist.csv = section_csv cls.playlist._update_sections() cls.participant = Participant.objects.create() - cls.block = Block.objects.create(rules='MATCHING_PAIRS', slug='mpairs', rounds=42) - cls.session = Session.objects.create( - block=cls.block, - participant=cls.participant, - playlist=cls.playlist - ) + cls.block = Block.objects.create(rules="MATCHING_PAIRS", slug="mpairs", rounds=42) + cls.session = Session.objects.create(block=cls.block, participant=cls.participant, playlist=cls.playlist) cls.rules = cls.session.block_rules() def test_next_round(self): @@ -59,8 +54,8 @@ def test_matching_pairs_trial(self): trial = self.rules.get_matching_pairs_trial(self.session) assert trial data = self.session.json_data - pairs = data.get('pairs') - degradations = data.get('degradations') + pairs = data.get("pairs") + degradations = data.get("degradations") # degradations cycle through list of two, list of one, empty list if i % 2 == 0: # there are 5 pairs available in total from the playlist; @@ -76,38 +71,40 @@ def test_matching_pairs_trial(self): assert len(degradations) == 0 def intermediate_score_request(self, data): - request_data = {'json_data': json.dumps( - data), **self.csrf_token, **self.session_data} - self.client.post( - '/result/intermediate_score/', request_data) - result = Result.objects.filter( - question_key='move').last() + request_data = {"json_data": json.dumps(data), **self.csrf_token, **self.session_data} + self.client.post("/result/intermediate_score/", request_data) + result = Result.objects.filter(question_key="move").last() return result def test_intermediate_score(self): - participant_info = json.loads(self.client.get('/participant/').content) - self.csrf_token = { - 'csrfmiddlewaretoken': participant_info.get('csrf_token')} - self.session.participant = Participant.objects.get( - pk=int(participant_info.get('id'))) + participant_info = json.loads(self.client.get("/participant/").content) + self.csrf_token = {"csrfmiddlewaretoken": participant_info.get("csrf_token")} + self.session.participant = Participant.objects.get(pk=int(participant_info.get("id"))) self.session.save() - self.session_data = {'session_id': self.session.id} + self.session_data = {"session_id": self.session.id} sections = self.playlist.section_set.all() - data = {'first_card': {'id': sections[0].id}, - 'second_card': {'id': sections[1].id}} + data = {"first_card": {"id": sections[0].id}, "second_card": {"id": sections[1].id}, "overlay_was_shown": False} result = self.intermediate_score_request(data) assert result.score == 10 - assert result.given_response == 'lucky match' - data['second_card'].update({'seen': True}) + assert result.given_response == "lucky match" + data["second_card"].update({"seen": True}) result = self.intermediate_score_request(data) assert result.score == 20 - assert result.given_response == 'match' - data['second_card'] = {'id': sections[3].id, 'seen': True} + assert result.given_response == "match" + data["second_card"] = {"id": sections[3].id, "seen": True} result = self.intermediate_score_request(data) assert result.score == -10 - assert result.given_response == 'misremembered' - data['first_card'].update({'seen': True}) - data['second_card'].pop('seen') + assert result.given_response == "misremembered" + data["first_card"].update({"seen": True}) + data["second_card"].pop("seen") result = self.intermediate_score_request(data) assert result.score == 0 - assert result.given_response == 'no match' + assert result.given_response == "no match" + + data["overlay_was_shown"] = True + result = self.intermediate_score_request(data) + assert result.json_data["overlay_was_shown"] is True + + data["overlay_was_shown"] = False + result = self.intermediate_score_request(data) + assert result.json_data["overlay_was_shown"] is False diff --git a/frontend/.pnp.cjs b/frontend/.pnp.cjs index 01a1706e8..75aebecd3 100755 --- a/frontend/.pnp.cjs +++ b/frontend/.pnp.cjs @@ -58,6 +58,8 @@ const RAW_RUNTIME_STATE = ["file-saver", "npm:2.0.5"],\ ["happy-dom", "npm:15.10.2"],\ ["history", "npm:5.3.0"],\ + ["msw", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:2.6.6"],\ + ["msw-storybook-addon", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:2.0.4"],\ ["prop-types", "npm:15.8.1"],\ ["qs", "npm:6.11.2"],\ ["react", "npm:18.3.1"],\ @@ -3469,6 +3471,37 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@bundled-es-modules/cookie", [\ + ["npm:2.0.1", {\ + "packageLocation": "../../../.yarn/berry/cache/@bundled-es-modules-cookie-npm-2.0.1-fb001bd2b9-10c0.zip/node_modules/@bundled-es-modules/cookie/",\ + "packageDependencies": [\ + ["@bundled-es-modules/cookie", "npm:2.0.1"],\ + ["cookie", "npm:0.7.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@bundled-es-modules/statuses", [\ + ["npm:1.0.1", {\ + "packageLocation": "../../../.yarn/berry/cache/@bundled-es-modules-statuses-npm-1.0.1-c6f8822c93-10c0.zip/node_modules/@bundled-es-modules/statuses/",\ + "packageDependencies": [\ + ["@bundled-es-modules/statuses", "npm:1.0.1"],\ + ["statuses", "npm:2.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@bundled-es-modules/tough-cookie", [\ + ["npm:0.1.6", {\ + "packageLocation": "../../../.yarn/berry/cache/@bundled-es-modules-tough-cookie-npm-0.1.6-aeb12ee45b-10c0.zip/node_modules/@bundled-es-modules/tough-cookie/",\ + "packageDependencies": [\ + ["@bundled-es-modules/tough-cookie", "npm:0.1.6"],\ + ["@types/tough-cookie", "npm:4.0.5"],\ + ["tough-cookie", "npm:4.1.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@chromatic-com/storybook", [\ ["npm:3.2.2", {\ "packageLocation": "../../../.yarn/berry/cache/@chromatic-com-storybook-npm-3.2.2-5a6acb70cc-10c0.zip/node_modules/@chromatic-com/storybook/",\ @@ -4167,6 +4200,75 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@inquirer/confirm", [\ + ["npm:5.0.2", {\ + "packageLocation": "../../../.yarn/berry/cache/@inquirer-confirm-npm-5.0.2-cb24775360-10c0.zip/node_modules/@inquirer/confirm/",\ + "packageDependencies": [\ + ["@inquirer/confirm", "npm:5.0.2"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:bc2343b0237acbbbfd07301eb563f1408bd9f9f07adc3ec2fd36933d63738b66cf90845fb98684e131aae35f492ab4dc29d3a5694caa272df5cd3ac024de8199#npm:5.0.2", {\ + "packageLocation": "./.yarn/__virtual__/@inquirer-confirm-virtual-72a55ee6b3/4/.yarn/berry/cache/@inquirer-confirm-npm-5.0.2-cb24775360-10c0.zip/node_modules/@inquirer/confirm/",\ + "packageDependencies": [\ + ["@inquirer/confirm", "virtual:bc2343b0237acbbbfd07301eb563f1408bd9f9f07adc3ec2fd36933d63738b66cf90845fb98684e131aae35f492ab4dc29d3a5694caa272df5cd3ac024de8199#npm:5.0.2"],\ + ["@inquirer/core", "npm:10.1.0"],\ + ["@inquirer/type", "virtual:572381c9a8388495067fc42be43ff6362e6c6b52126523c9bb77573f32b927ee671084b4aa78e82c9127bb29e716c6a5afcea14d7634218f50290dcc55b1ee63#npm:3.0.1"],\ + ["@types/node", null]\ + ],\ + "packagePeers": [\ + "@types/node"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@inquirer/core", [\ + ["npm:10.1.0", {\ + "packageLocation": "../../../.yarn/berry/cache/@inquirer-core-npm-10.1.0-572381c9a8-10c0.zip/node_modules/@inquirer/core/",\ + "packageDependencies": [\ + ["@inquirer/core", "npm:10.1.0"],\ + ["@inquirer/figures", "npm:1.0.8"],\ + ["@inquirer/type", "virtual:572381c9a8388495067fc42be43ff6362e6c6b52126523c9bb77573f32b927ee671084b4aa78e82c9127bb29e716c6a5afcea14d7634218f50290dcc55b1ee63#npm:3.0.1"],\ + ["ansi-escapes", "npm:4.3.2"],\ + ["cli-width", "npm:4.1.0"],\ + ["mute-stream", "npm:2.0.0"],\ + ["signal-exit", "npm:4.1.0"],\ + ["strip-ansi", "npm:6.0.1"],\ + ["wrap-ansi", "npm:6.2.0"],\ + ["yoctocolors-cjs", "npm:2.1.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@inquirer/figures", [\ + ["npm:1.0.8", {\ + "packageLocation": "../../../.yarn/berry/cache/@inquirer-figures-npm-1.0.8-b84d6f580a-10c0.zip/node_modules/@inquirer/figures/",\ + "packageDependencies": [\ + ["@inquirer/figures", "npm:1.0.8"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@inquirer/type", [\ + ["npm:3.0.1", {\ + "packageLocation": "../../../.yarn/berry/cache/@inquirer-type-npm-3.0.1-5402bd13af-10c0.zip/node_modules/@inquirer/type/",\ + "packageDependencies": [\ + ["@inquirer/type", "npm:3.0.1"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:572381c9a8388495067fc42be43ff6362e6c6b52126523c9bb77573f32b927ee671084b4aa78e82c9127bb29e716c6a5afcea14d7634218f50290dcc55b1ee63#npm:3.0.1", {\ + "packageLocation": "./.yarn/__virtual__/@inquirer-type-virtual-27f6485a34/4/.yarn/berry/cache/@inquirer-type-npm-3.0.1-5402bd13af-10c0.zip/node_modules/@inquirer/type/",\ + "packageDependencies": [\ + ["@inquirer/type", "virtual:572381c9a8388495067fc42be43ff6362e6c6b52126523c9bb77573f32b927ee671084b4aa78e82c9127bb29e716c6a5afcea14d7634218f50290dcc55b1ee63#npm:3.0.1"],\ + ["@types/node", null]\ + ],\ + "packagePeers": [\ + "@types/node"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@isaacs/cliui", [\ ["npm:8.0.2", {\ "packageLocation": "../../../.yarn/berry/cache/@isaacs-cliui-npm-8.0.2-f4364666d5-10c0.zip/node_modules/@isaacs/cliui/",\ @@ -4334,6 +4436,21 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@mswjs/interceptors", [\ + ["npm:0.37.3", {\ + "packageLocation": "../../../.yarn/berry/cache/@mswjs-interceptors-npm-0.37.3-bb1c9ca629-10c0.zip/node_modules/@mswjs/interceptors/",\ + "packageDependencies": [\ + ["@mswjs/interceptors", "npm:0.37.3"],\ + ["@open-draft/deferred-promise", "npm:2.2.0"],\ + ["@open-draft/logger", "npm:0.3.0"],\ + ["@open-draft/until", "npm:2.1.0"],\ + ["is-node-process", "npm:1.2.0"],\ + ["outvariant", "npm:1.4.3"],\ + ["strict-event-emitter", "npm:0.5.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@nicolo-ribaudo/eslint-scope-5-internals", [\ ["npm:5.1.1-v1", {\ "packageLocation": "../../../.yarn/berry/cache/@nicolo-ribaudo-eslint-scope-5-internals-npm-5.1.1-v1-87df86be4b-10c0.zip/node_modules/@nicolo-ribaudo/eslint-scope-5-internals/",\ @@ -4399,6 +4516,35 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@open-draft/deferred-promise", [\ + ["npm:2.2.0", {\ + "packageLocation": "../../../.yarn/berry/cache/@open-draft-deferred-promise-npm-2.2.0-adf396dc9f-10c0.zip/node_modules/@open-draft/deferred-promise/",\ + "packageDependencies": [\ + ["@open-draft/deferred-promise", "npm:2.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@open-draft/logger", [\ + ["npm:0.3.0", {\ + "packageLocation": "../../../.yarn/berry/cache/@open-draft-logger-npm-0.3.0-12b03e55aa-10c0.zip/node_modules/@open-draft/logger/",\ + "packageDependencies": [\ + ["@open-draft/logger", "npm:0.3.0"],\ + ["is-node-process", "npm:1.2.0"],\ + ["outvariant", "npm:1.4.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@open-draft/until", [\ + ["npm:2.1.0", {\ + "packageLocation": "../../../.yarn/berry/cache/@open-draft-until-npm-2.1.0-e27da33c52-10c0.zip/node_modules/@open-draft/until/",\ + "packageDependencies": [\ + ["@open-draft/until", "npm:2.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@pkgjs/parseargs", [\ ["npm:0.11.0", {\ "packageLocation": "../../../.yarn/berry/cache/@pkgjs-parseargs-npm-0.11.0-cd2a3fe948-10c0.zip/node_modules/@pkgjs/parseargs/",\ @@ -5736,6 +5882,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/cookie", [\ + ["npm:0.6.0", {\ + "packageLocation": "../../../.yarn/berry/cache/@types-cookie-npm-0.6.0-1f4c3f48f0-10c0.zip/node_modules/@types/cookie/",\ + "packageDependencies": [\ + ["@types/cookie", "npm:0.6.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/doctrine", [\ ["npm:0.0.9", {\ "packageLocation": "../../../.yarn/berry/cache/@types-doctrine-npm-0.0.9-ffe93045db-10c0.zip/node_modules/@types/doctrine/",\ @@ -5955,6 +6110,24 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/statuses", [\ + ["npm:2.0.5", {\ + "packageLocation": "../../../.yarn/berry/cache/@types-statuses-npm-2.0.5-f46121f53f-10c0.zip/node_modules/@types/statuses/",\ + "packageDependencies": [\ + ["@types/statuses", "npm:2.0.5"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@types/tough-cookie", [\ + ["npm:4.0.5", {\ + "packageLocation": "../../../.yarn/berry/cache/@types-tough-cookie-npm-4.0.5-8c5e2162e1-10c0.zip/node_modules/@types/tough-cookie/",\ + "packageDependencies": [\ + ["@types/tough-cookie", "npm:4.0.5"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/uuid", [\ ["npm:9.0.7", {\ "packageLocation": "../../../.yarn/berry/cache/@types-uuid-npm-9.0.7-c380bb8654-10c0.zip/node_modules/@types/uuid/",\ @@ -6633,6 +6806,8 @@ const RAW_RUNTIME_STATE = ["file-saver", "npm:2.0.5"],\ ["happy-dom", "npm:15.10.2"],\ ["history", "npm:5.3.0"],\ + ["msw", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:2.6.6"],\ + ["msw-storybook-addon", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:2.0.4"],\ ["prop-types", "npm:15.8.1"],\ ["qs", "npm:6.11.2"],\ ["react", "npm:18.3.1"],\ @@ -6655,6 +6830,16 @@ const RAW_RUNTIME_STATE = "linkType": "SOFT"\ }]\ ]],\ + ["ansi-escapes", [\ + ["npm:4.3.2", {\ + "packageLocation": "../../../.yarn/berry/cache/ansi-escapes-npm-4.3.2-3ad173702f-10c0.zip/node_modules/ansi-escapes/",\ + "packageDependencies": [\ + ["ansi-escapes", "npm:4.3.2"],\ + ["type-fest", "npm:0.21.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["ansi-regex", [\ ["npm:5.0.1", {\ "packageLocation": "../../../.yarn/berry/cache/ansi-regex-npm-5.0.1-c963a48615-10c0.zip/node_modules/ansi-regex/",\ @@ -7371,6 +7556,27 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["cli-width", [\ + ["npm:4.1.0", {\ + "packageLocation": "../../../.yarn/berry/cache/cli-width-npm-4.1.0-c08b53be83-10c0.zip/node_modules/cli-width/",\ + "packageDependencies": [\ + ["cli-width", "npm:4.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["cliui", [\ + ["npm:8.0.1", {\ + "packageLocation": "../../../.yarn/berry/cache/cliui-npm-8.0.1-3b029092cf-10c0.zip/node_modules/cliui/",\ + "packageDependencies": [\ + ["cliui", "npm:8.0.1"],\ + ["string-width", "npm:4.2.3"],\ + ["strip-ansi", "npm:6.0.1"],\ + ["wrap-ansi", "npm:7.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["color-convert", [\ ["npm:1.9.3", {\ "packageLocation": "../../../.yarn/berry/cache/color-convert-npm-1.9.3-1fe690075e-10c0.zip/node_modules/color-convert/",\ @@ -7449,6 +7655,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["cookie", [\ + ["npm:0.7.2", {\ + "packageLocation": "../../../.yarn/berry/cache/cookie-npm-0.7.2-6ea9ee4231-10c0.zip/node_modules/cookie/",\ + "packageDependencies": [\ + ["cookie", "npm:0.7.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["core-js-compat", [\ ["npm:3.32.1", {\ "packageLocation": "../../../.yarn/berry/cache/core-js-compat-npm-3.32.1-d74aca93d6-10c0.zip/node_modules/core-js-compat/",\ @@ -8892,6 +9107,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["get-caller-file", [\ + ["npm:2.0.5", {\ + "packageLocation": "../../../.yarn/berry/cache/get-caller-file-npm-2.0.5-80e8a86305-10c0.zip/node_modules/get-caller-file/",\ + "packageDependencies": [\ + ["get-caller-file", "npm:2.0.5"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["get-intrinsic", [\ ["npm:1.2.1", {\ "packageLocation": "../../../.yarn/berry/cache/get-intrinsic-npm-1.2.1-ae857fd610-10c0.zip/node_modules/get-intrinsic/",\ @@ -9053,6 +9277,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["graphql", [\ + ["npm:16.9.0", {\ + "packageLocation": "../../../.yarn/berry/cache/graphql-npm-16.9.0-a36f71845f-10c0.zip/node_modules/graphql/",\ + "packageDependencies": [\ + ["graphql", "npm:16.9.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["happy-dom", [\ ["npm:15.10.2", {\ "packageLocation": "../../../.yarn/berry/cache/happy-dom-npm-15.10.2-3eadf189bc-10c0.zip/node_modules/happy-dom/",\ @@ -9138,6 +9371,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["headers-polyfill", [\ + ["npm:4.0.3", {\ + "packageLocation": "../../../.yarn/berry/cache/headers-polyfill-npm-4.0.3-65ca63b329-10c0.zip/node_modules/headers-polyfill/",\ + "packageDependencies": [\ + ["headers-polyfill", "npm:4.0.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["history", [\ ["npm:5.3.0", {\ "packageLocation": "../../../.yarn/berry/cache/history-npm-5.3.0-00136b6a63-10c0.zip/node_modules/history/",\ @@ -9491,6 +9733,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["is-node-process", [\ + ["npm:1.2.0", {\ + "packageLocation": "../../../.yarn/berry/cache/is-node-process-npm-1.2.0-34f2abe8e1-10c0.zip/node_modules/is-node-process/",\ + "packageDependencies": [\ + ["is-node-process", "npm:1.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["is-number", [\ ["npm:7.0.0", {\ "packageLocation": "../../../.yarn/berry/cache/is-number-npm-7.0.0-060086935c-10c0.zip/node_modules/is-number/",\ @@ -10338,6 +10589,78 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["msw", [\ + ["npm:2.6.6", {\ + "packageLocation": "./.yarn/unplugged/msw-virtual-bc2343b023/node_modules/msw/",\ + "packageDependencies": [\ + ["msw", "npm:2.6.6"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:2.6.6", {\ + "packageLocation": "./.yarn/unplugged/msw-virtual-bc2343b023/node_modules/msw/",\ + "packageDependencies": [\ + ["msw", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:2.6.6"],\ + ["@bundled-es-modules/cookie", "npm:2.0.1"],\ + ["@bundled-es-modules/statuses", "npm:1.0.1"],\ + ["@bundled-es-modules/tough-cookie", "npm:0.1.6"],\ + ["@inquirer/confirm", "virtual:bc2343b0237acbbbfd07301eb563f1408bd9f9f07adc3ec2fd36933d63738b66cf90845fb98684e131aae35f492ab4dc29d3a5694caa272df5cd3ac024de8199#npm:5.0.2"],\ + ["@mswjs/interceptors", "npm:0.37.3"],\ + ["@open-draft/deferred-promise", "npm:2.2.0"],\ + ["@open-draft/until", "npm:2.1.0"],\ + ["@types/cookie", "npm:0.6.0"],\ + ["@types/statuses", "npm:2.0.5"],\ + ["@types/typescript", null],\ + ["chalk", "npm:4.1.2"],\ + ["graphql", "npm:16.9.0"],\ + ["headers-polyfill", "npm:4.0.3"],\ + ["is-node-process", "npm:1.2.0"],\ + ["outvariant", "npm:1.4.3"],\ + ["path-to-regexp", "npm:6.3.0"],\ + ["strict-event-emitter", "npm:0.5.1"],\ + ["type-fest", "npm:4.30.0"],\ + ["typescript", "patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7"],\ + ["yargs", "npm:17.7.2"]\ + ],\ + "packagePeers": [\ + "@types/typescript",\ + "typescript"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["msw-storybook-addon", [\ + ["npm:2.0.4", {\ + "packageLocation": "../../../.yarn/berry/cache/msw-storybook-addon-npm-2.0.4-4c4d69bc12-10c0.zip/node_modules/msw-storybook-addon/",\ + "packageDependencies": [\ + ["msw-storybook-addon", "npm:2.0.4"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:2.0.4", {\ + "packageLocation": "./.yarn/__virtual__/msw-storybook-addon-virtual-95b6ac0886/4/.yarn/berry/cache/msw-storybook-addon-npm-2.0.4-4c4d69bc12-10c0.zip/node_modules/msw-storybook-addon/",\ + "packageDependencies": [\ + ["msw-storybook-addon", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:2.0.4"],\ + ["@types/msw", null],\ + ["is-node-process", "npm:1.2.0"],\ + ["msw", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:2.6.6"]\ + ],\ + "packagePeers": [\ + "@types/msw",\ + "msw"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["mute-stream", [\ + ["npm:2.0.0", {\ + "packageLocation": "../../../.yarn/berry/cache/mute-stream-npm-2.0.0-45d3c1ef83-10c0.zip/node_modules/mute-stream/",\ + "packageDependencies": [\ + ["mute-stream", "npm:2.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["nanoid", [\ ["npm:3.3.8", {\ "packageLocation": "../../../.yarn/berry/cache/nanoid-npm-3.3.8-d22226208b-10c0.zip/node_modules/nanoid/",\ @@ -10565,6 +10888,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["outvariant", [\ + ["npm:1.4.3", {\ + "packageLocation": "../../../.yarn/berry/cache/outvariant-npm-1.4.3-192f951f81-10c0.zip/node_modules/outvariant/",\ + "packageDependencies": [\ + ["outvariant", "npm:1.4.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["p-limit", [\ ["npm:3.1.0", {\ "packageLocation": "../../../.yarn/berry/cache/p-limit-npm-3.1.0-05d2ede37f-10c0.zip/node_modules/p-limit/",\ @@ -10683,6 +11015,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["path-to-regexp", [\ + ["npm:6.3.0", {\ + "packageLocation": "../../../.yarn/berry/cache/path-to-regexp-npm-6.3.0-ee2cdde576-10c0.zip/node_modules/path-to-regexp/",\ + "packageDependencies": [\ + ["path-to-regexp", "npm:6.3.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["path-type", [\ ["npm:4.0.0", {\ "packageLocation": "../../../.yarn/berry/cache/path-type-npm-4.0.0-10d47fc86a-10c0.zip/node_modules/path-type/",\ @@ -10828,6 +11169,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["psl", [\ + ["npm:1.15.0", {\ + "packageLocation": "../../../.yarn/berry/cache/psl-npm-1.15.0-410584ca6b-10c0.zip/node_modules/psl/",\ + "packageDependencies": [\ + ["psl", "npm:1.15.0"],\ + ["punycode", "npm:2.3.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["punycode", [\ ["npm:2.3.0", {\ "packageLocation": "../../../.yarn/berry/cache/punycode-npm-2.3.0-df4bdce06b-10c0.zip/node_modules/punycode/",\ @@ -10835,6 +11186,13 @@ const RAW_RUNTIME_STATE = ["punycode", "npm:2.3.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.3.1", {\ + "packageLocation": "../../../.yarn/berry/cache/punycode-npm-2.3.1-97543c420d-10c0.zip/node_modules/punycode/",\ + "packageDependencies": [\ + ["punycode", "npm:2.3.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["qs", [\ @@ -10847,6 +11205,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["querystringify", [\ + ["npm:2.2.0", {\ + "packageLocation": "../../../.yarn/berry/cache/querystringify-npm-2.2.0-4e77c9f606-10c0.zip/node_modules/querystringify/",\ + "packageDependencies": [\ + ["querystringify", "npm:2.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["queue-microtask", [\ ["npm:1.2.3", {\ "packageLocation": "../../../.yarn/berry/cache/queue-microtask-npm-1.2.3-fcc98e4e2d-10c0.zip/node_modules/queue-microtask/",\ @@ -11355,6 +11722,24 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["require-directory", [\ + ["npm:2.1.1", {\ + "packageLocation": "../../../.yarn/berry/cache/require-directory-npm-2.1.1-8608aee50b-10c0.zip/node_modules/require-directory/",\ + "packageDependencies": [\ + ["require-directory", "npm:2.1.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["requires-port", [\ + ["npm:1.0.0", {\ + "packageLocation": "../../../.yarn/berry/cache/requires-port-npm-1.0.0-fd036b488a-10c0.zip/node_modules/requires-port/",\ + "packageDependencies": [\ + ["requires-port", "npm:1.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["resize-observer-polyfill", [\ ["npm:1.5.1", {\ "packageLocation": "../../../.yarn/berry/cache/resize-observer-polyfill-npm-1.5.1-603120e8a0-10c0.zip/node_modules/resize-observer-polyfill/",\ @@ -11720,6 +12105,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["statuses", [\ + ["npm:2.0.1", {\ + "packageLocation": "../../../.yarn/berry/cache/statuses-npm-2.0.1-81d2b97fee-10c0.zip/node_modules/statuses/",\ + "packageDependencies": [\ + ["statuses", "npm:2.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["std-env", [\ ["npm:3.7.0", {\ "packageLocation": "../../../.yarn/berry/cache/std-env-npm-3.7.0-5261c3c3c3-10c0.zip/node_modules/std-env/",\ @@ -11752,6 +12146,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["strict-event-emitter", [\ + ["npm:0.5.1", {\ + "packageLocation": "../../../.yarn/berry/cache/strict-event-emitter-npm-0.5.1-8414bf36b3-10c0.zip/node_modules/strict-event-emitter/",\ + "packageDependencies": [\ + ["strict-event-emitter", "npm:0.5.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["string-natural-compare", [\ ["npm:3.0.1", {\ "packageLocation": "../../../.yarn/berry/cache/string-natural-compare-npm-3.0.1-f6d0be6457-10c0.zip/node_modules/string-natural-compare/",\ @@ -12042,6 +12445,19 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["tough-cookie", [\ + ["npm:4.1.4", {\ + "packageLocation": "../../../.yarn/berry/cache/tough-cookie-npm-4.1.4-8293cc8bd5-10c0.zip/node_modules/tough-cookie/",\ + "packageDependencies": [\ + ["tough-cookie", "npm:4.1.4"],\ + ["psl", "npm:1.15.0"],\ + ["punycode", "npm:2.3.1"],\ + ["universalify", "npm:0.2.0"],\ + ["url-parse", "npm:1.5.10"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["ts-api-utils", [\ ["npm:1.4.0", {\ "packageLocation": "../../../.yarn/berry/cache/ts-api-utils-npm-1.4.0-b091964d6e-10c0.zip/node_modules/ts-api-utils/",\ @@ -12198,12 +12614,26 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ + ["npm:0.21.3", {\ + "packageLocation": "../../../.yarn/berry/cache/type-fest-npm-0.21.3-5ff2a9c6fd-10c0.zip/node_modules/type-fest/",\ + "packageDependencies": [\ + ["type-fest", "npm:0.21.3"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:2.19.0", {\ "packageLocation": "../../../.yarn/berry/cache/type-fest-npm-2.19.0-918b953248-10c0.zip/node_modules/type-fest/",\ "packageDependencies": [\ ["type-fest", "npm:2.19.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:4.30.0", {\ + "packageLocation": "../../../.yarn/berry/cache/type-fest-npm-4.30.0-b4b33e2412-10c0.zip/node_modules/type-fest/",\ + "packageDependencies": [\ + ["type-fest", "npm:4.30.0"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["typed-array-buffer", [\ @@ -12338,6 +12768,13 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["universalify", [\ + ["npm:0.2.0", {\ + "packageLocation": "../../../.yarn/berry/cache/universalify-npm-0.2.0-9984e61c10-10c0.zip/node_modules/universalify/",\ + "packageDependencies": [\ + ["universalify", "npm:0.2.0"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:2.0.0", {\ "packageLocation": "../../../.yarn/berry/cache/universalify-npm-2.0.0-03b8b418a8-10c0.zip/node_modules/universalify/",\ "packageDependencies": [\ @@ -12415,6 +12852,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["url-parse", [\ + ["npm:1.5.10", {\ + "packageLocation": "../../../.yarn/berry/cache/url-parse-npm-1.5.10-64fa2bcd6d-10c0.zip/node_modules/url-parse/",\ + "packageDependencies": [\ + ["url-parse", "npm:1.5.10"],\ + ["querystringify", "npm:2.2.0"],\ + ["requires-port", "npm:1.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["use-isomorphic-layout-effect", [\ ["npm:1.1.2", {\ "packageLocation": "../../../.yarn/berry/cache/use-isomorphic-layout-effect-npm-1.1.2-65facd0a4b-10c0.zip/node_modules/use-isomorphic-layout-effect/",\ @@ -12815,6 +13263,16 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["wrap-ansi", [\ + ["npm:6.2.0", {\ + "packageLocation": "../../../.yarn/berry/cache/wrap-ansi-npm-6.2.0-439a7246d8-10c0.zip/node_modules/wrap-ansi/",\ + "packageDependencies": [\ + ["wrap-ansi", "npm:6.2.0"],\ + ["ansi-styles", "npm:4.3.0"],\ + ["string-width", "npm:4.2.3"],\ + ["strip-ansi", "npm:6.0.1"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:7.0.0", {\ "packageLocation": "../../../.yarn/berry/cache/wrap-ansi-npm-7.0.0-ad6e1a0554-10c0.zip/node_modules/wrap-ansi/",\ "packageDependencies": [\ @@ -12871,6 +13329,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["y18n", [\ + ["npm:5.0.8", {\ + "packageLocation": "../../../.yarn/berry/cache/y18n-npm-5.0.8-5f3a0a7e62-10c0.zip/node_modules/y18n/",\ + "packageDependencies": [\ + ["y18n", "npm:5.0.8"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["yallist", [\ ["npm:3.1.1", {\ "packageLocation": "../../../.yarn/berry/cache/yallist-npm-3.1.1-a568a556b4-10c0.zip/node_modules/yallist/",\ @@ -12896,6 +13363,31 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["yargs", [\ + ["npm:17.7.2", {\ + "packageLocation": "../../../.yarn/berry/cache/yargs-npm-17.7.2-80b62638e1-10c0.zip/node_modules/yargs/",\ + "packageDependencies": [\ + ["yargs", "npm:17.7.2"],\ + ["cliui", "npm:8.0.1"],\ + ["escalade", "npm:3.1.1"],\ + ["get-caller-file", "npm:2.0.5"],\ + ["require-directory", "npm:2.1.1"],\ + ["string-width", "npm:4.2.3"],\ + ["y18n", "npm:5.0.8"],\ + ["yargs-parser", "npm:21.1.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["yargs-parser", [\ + ["npm:21.1.1", {\ + "packageLocation": "../../../.yarn/berry/cache/yargs-parser-npm-21.1.1-8fdc003314-10c0.zip/node_modules/yargs-parser/",\ + "packageDependencies": [\ + ["yargs-parser", "npm:21.1.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["yocto-queue", [\ ["npm:0.1.0", {\ "packageLocation": "../../../.yarn/berry/cache/yocto-queue-npm-0.1.0-c6c9a7db29-10c0.zip/node_modules/yocto-queue/",\ @@ -12905,6 +13397,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["yoctocolors-cjs", [\ + ["npm:2.1.2", {\ + "packageLocation": "../../../.yarn/berry/cache/yoctocolors-cjs-npm-2.1.2-52d47e1a9b-10c0.zip/node_modules/yoctocolors-cjs/",\ + "packageDependencies": [\ + ["yoctocolors-cjs", "npm:2.1.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["zustand", [\ ["npm:4.4.7", {\ "packageLocation": "../../../.yarn/berry/cache/zustand-npm-4.4.7-974264f8cd-10c0.zip/node_modules/zustand/",\ diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js index b2a75c67f..c40ed9dc1 100644 --- a/frontend/.storybook/preview.js +++ b/frontend/.storybook/preview.js @@ -1,3 +1,4 @@ +import { initialize, mswLoader } from 'msw-storybook-addon' import "../public/vendor/bootstrap/bootstrap.min.css"; import "../src/index.scss"; import { initAudioListener } from "../src/util/audio"; @@ -7,6 +8,9 @@ import { initWebAudioListener } from "../src/util/webAudio"; initAudioListener(); initWebAudioListener(); +// Initialize MSW +initialize() + /** @type { import('@storybook/react').Preview } */ const preview = { parameters: { @@ -17,6 +21,8 @@ const preview = { }, }, }, + // Provide the MSW addon loader globally + loaders: [mswLoader], }; diff --git a/frontend/package.json b/frontend/package.json index ead449e27..403348ada 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -96,8 +96,15 @@ "eslint-plugin-storybook": "^0.11.1", "happy-dom": "^15.10.2", "history": "^5.3.0", + "msw": "^2.6.6", + "msw-storybook-addon": "^2.0.4", "prop-types": "15.8.1", "storybook": "^8.4.6", "vitest": "^2.1.4" + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js new file mode 100644 index 000000000..fead0b3ff --- /dev/null +++ b/frontend/public/mockServiceWorker.js @@ -0,0 +1,295 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.6.6' +const INTEGRITY_CHECKSUM = 'ca7800994cc8bfb5eb961e037c877074' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + headers.delete('accept', 'msw/passthrough') + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/frontend/src/components/Button/Button.tsx b/frontend/src/components/Button/Button.tsx index 9d5b5725d..c64e7dc82 100644 --- a/frontend/src/components/Button/Button.tsx +++ b/frontend/src/components/Button/Button.tsx @@ -10,9 +10,10 @@ interface ButtonProps { style?: React.CSSProperties; disabled?: boolean; value?: string; + clickOnce?: boolean; } -// Button is a button that can only be clicked one time +// Button is a button that can only be clicked one time by default const Button = ({ title, onClick, @@ -21,11 +22,17 @@ const Button = ({ style = {}, disabled = false, value = "", + clickOnce = true, }: ButtonProps) => { const clicked = useRef(false); // Only handle the first click - const clickOnce = () => { + const clickOnceGuard = () => { + + if (!clickOnce) { + return onClick(value); + } + if (disabled || clicked.current) { return; } @@ -38,9 +45,9 @@ const Button = ({ // Without the browser having registered any user interaction (e.g. click) const touchEvent = audioInitialized ? { - onTouchStart: (e) => { + onTouchStart: (e: React.TouchEvent) => { e.stopPropagation(); - clickOnce(); + clickOnceGuard(); return false; }, } @@ -50,13 +57,13 @@ const Button = ({ + +
+

{title}

+
+ +
+ {content} +
+ +
+
+ + + ); +}; + +export default Overlay; diff --git a/frontend/src/components/Playback/Playback.tsx b/frontend/src/components/Playback/Playback.tsx index 1a7bcd75a..d35276b82 100644 --- a/frontend/src/components/Playback/Playback.tsx +++ b/frontend/src/components/Playback/Playback.tsx @@ -238,11 +238,10 @@ const Playback = ({ return ( ); default: diff --git a/frontend/src/stories/MatchingPairs.stories.jsx b/frontend/src/stories/MatchingPairs.stories.tsx similarity index 87% rename from frontend/src/stories/MatchingPairs.stories.jsx rename to frontend/src/stories/MatchingPairs.stories.tsx index 37734fd59..4779bfbad 100644 --- a/frontend/src/stories/MatchingPairs.stories.jsx +++ b/frontend/src/stories/MatchingPairs.stories.tsx @@ -1,7 +1,11 @@ +import { http, HttpResponse } from 'msw' import useBoundStore from "@/util/stores"; import MatchingPairs, { SCORE_FEEDBACK_DISPLAY } from "../components/MatchingPairs/MatchingPairs"; + import audio from "./assets/audio.wav"; +import { API_BASE_URL } from '@/config'; +import { URLS } from '@/API'; const StoreDecorator = (Story) => { const setSession = useBoundStore((state) => state.setSession); @@ -34,7 +38,7 @@ export default { }; const getDefaultArgs = (overrides = {}) => ({ - playSection: () => {}, + playSection: () => { }, sections: [ { id: 1, @@ -94,9 +98,20 @@ const getDefaultArgs = (overrides = {}) => ({ }, ], playerIndex: 0, - stopAudio: () => {}, - submitResult: () => {}, - finishedPlaying: () => {}, + stopAudio: () => { }, + submitResult: () => { }, + finishedPlaying: () => { }, + ...overrides, +}); + +const getDefaultParams = (overrides = {}) => ({ + msw: { + handlers: [ + http.post(API_BASE_URL + URLS.result.intermediateScore, () => { + return HttpResponse.json({ score: 10 }); + }) + ], + }, ...overrides, }); @@ -105,13 +120,13 @@ export const Default = { ...getDefaultArgs(), }, decorators: [StoreDecorator], - parameters: { + parameters: getDefaultParams({ docs: { description: { component: "This story shows the component with the default props.", }, }, - }, + }), }; export const WithThreeColumns = { @@ -162,14 +177,14 @@ export const WithThreeColumns = { ], }), decorators: [StoreDecorator], - parameters: { + parameters: getDefaultParams({ docs: { description: { component: "This story shows the component with three columns. The component automatically adjusts the number of columns based on the number of sections. Six or less sections will result in three columns, more than six sections will result in four columns.", }, }, - }, + }), }; export const WithSmallBottomRightScoreFeedback = { @@ -178,13 +193,13 @@ export const WithSmallBottomRightScoreFeedback = { scoreFeedbackDisplay: SCORE_FEEDBACK_DISPLAY.SMALL_BOTTOM_RIGHT, }, decorators: [StoreDecorator], - parameters: { + parameters: getDefaultParams({ docs: { description: { component: "This story shows the component with the default props.", }, }, - }, + }), }; export const WithShowAnimation = { @@ -193,11 +208,11 @@ export const WithShowAnimation = { showAnimation: true, }, decorators: [StoreDecorator], - parameters: { + parameters: getDefaultParams({ docs: { description: { component: "This story shows the component with the default props.", }, }, - }, + }), }; diff --git a/frontend/src/stories/Overlay.stories.tsx b/frontend/src/stories/Overlay.stories.tsx new file mode 100644 index 000000000..66dcba716 --- /dev/null +++ b/frontend/src/stories/Overlay.stories.tsx @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Overlay from '@/components/Overlay/Overlay'; +import { useState } from 'react'; +import Button from '@/components/Button/Button'; + +const meta: Meta = { + title: 'Components/Overlay', + component: Overlay, + parameters: { + }, + decorators: [ + (Story, context) => { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+
+ ) + } + ], +}; + +export default meta; +type Story = StoryObj; + +const defaultOverlayProps = { + isOpen: false, + title: 'Tutorial', + content: 'This is a tutorial.', + onClose: () => { }, +}; + +export const Default: Story = { + args: { + ...defaultOverlayProps, + }, +}; + +export const WithLongContent: Story = { + args: { + ...defaultOverlayProps, + content: ( +
+

Welcome to our app!

+

Here's how to get started:

+
    +
  1. First, create your profile
  2. +
  3. Then, explore our features
  4. +
  5. Finally, start creating!
  6. +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +

+
+ ), + }, +}; + +export const CustomTitle: Story = { + args: { + ...defaultOverlayProps, + title: '🎉 Getting Started', + content: 'Welcome to our awesome app!', + }, +}; diff --git a/frontend/src/types/Playback.ts b/frontend/src/types/Playback.ts index 20b2cb049..be60974d5 100644 --- a/frontend/src/types/Playback.ts +++ b/frontend/src/types/Playback.ts @@ -35,4 +35,5 @@ export interface PlaybackArgs { stop_audio_after?: number; timeout_after_playback?: number; score_feedback_display?: string; + tutorial?: { [key: string]: string }; } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 083e0bf49..26c6bf0b2 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2130,6 +2130,34 @@ __metadata: languageName: node linkType: hard +"@bundled-es-modules/cookie@npm:^2.0.1": + version: 2.0.1 + resolution: "@bundled-es-modules/cookie@npm:2.0.1" + dependencies: + cookie: "npm:^0.7.2" + checksum: 10c0/dfac5e36127e827c5557b8577f17a8aa94c057baff6d38555917927b99da0ecf0b1357e7fedadc8853ecdbd4a8a7fa1f5e64111b2a656612f4a36376f5bdbe8d + languageName: node + linkType: hard + +"@bundled-es-modules/statuses@npm:^1.0.1": + version: 1.0.1 + resolution: "@bundled-es-modules/statuses@npm:1.0.1" + dependencies: + statuses: "npm:^2.0.1" + checksum: 10c0/c1a8ede3efa8da61ccda4b98e773582a9733edfbeeee569d4630785f8e018766202edb190a754a3ec7a7f6bd738e857829affc2fdb676b6dab4db1bb44e62785 + languageName: node + linkType: hard + +"@bundled-es-modules/tough-cookie@npm:^0.1.6": + version: 0.1.6 + resolution: "@bundled-es-modules/tough-cookie@npm:0.1.6" + dependencies: + "@types/tough-cookie": "npm:^4.0.5" + tough-cookie: "npm:^4.1.4" + checksum: 10c0/28bcac878bff6b34719ba3aa8341e9924772ee55de5487680ebe784981ec9fccb70ed5d46f563e2404855a04de606f9e56aa4202842d4f5835bc04a4fe820571 + languageName: node + linkType: hard + "@chromatic-com/storybook@npm:^3.2.2": version: 3.2.2 resolution: "@chromatic-com/storybook@npm:3.2.2" @@ -2695,6 +2723,51 @@ __metadata: languageName: node linkType: hard +"@inquirer/confirm@npm:^5.0.0": + version: 5.0.2 + resolution: "@inquirer/confirm@npm:5.0.2" + dependencies: + "@inquirer/core": "npm:^10.1.0" + "@inquirer/type": "npm:^3.0.1" + peerDependencies: + "@types/node": ">=18" + checksum: 10c0/c121cfb0557b42dd6570b54dce707a048d85f328481d5230d21fede195902012ede06887aa478875cc83afa064c2e30953eb2cab0744f832195867b418865115 + languageName: node + linkType: hard + +"@inquirer/core@npm:^10.1.0": + version: 10.1.0 + resolution: "@inquirer/core@npm:10.1.0" + dependencies: + "@inquirer/figures": "npm:^1.0.8" + "@inquirer/type": "npm:^3.0.1" + ansi-escapes: "npm:^4.3.2" + cli-width: "npm:^4.1.0" + mute-stream: "npm:^2.0.0" + signal-exit: "npm:^4.1.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^6.2.0" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/ffd187edb210426c3e25ed564f7aa8844468c28dd2ba3c53dbe28d3359b519cdfae987b31bf927c1dd2e9f70a914fdefe319abe4c5f384e5e08410d11e0a7ce2 + languageName: node + linkType: hard + +"@inquirer/figures@npm:^1.0.8": + version: 1.0.8 + resolution: "@inquirer/figures@npm:1.0.8" + checksum: 10c0/34d287ff1fd16476c58bbd5b169db315f8319b5ffb09f81a1bb9aabd4165114e7406b1f418d021fd9cd48923008446e3eec274bb818f378ea132a0450bbc91d4 + languageName: node + linkType: hard + +"@inquirer/type@npm:^3.0.1": + version: 3.0.1 + resolution: "@inquirer/type@npm:3.0.1" + peerDependencies: + "@types/node": ">=18" + checksum: 10c0/c8612362d382114a318dbb523de7b1f54dc6bc6d3016c6eaf299b6a32486b92b0dfb1b4cfc6fe9d99496d15fbb721873a1bd66819f796c8bb09853a3b808812d + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -2821,6 +2894,20 @@ __metadata: languageName: node linkType: hard +"@mswjs/interceptors@npm:^0.37.0": + version: 0.37.3 + resolution: "@mswjs/interceptors@npm:0.37.3" + dependencies: + "@open-draft/deferred-promise": "npm:^2.2.0" + "@open-draft/logger": "npm:^0.3.0" + "@open-draft/until": "npm:^2.0.0" + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.4.3" + strict-event-emitter: "npm:^0.5.1" + checksum: 10c0/5a8d9ab7c491d14dff996f23bda0fa7b7059f68a1d4981b804b6114f1c0c0490bc35860df135ed36da1720984323878b1a9d13dbc80936bbfa67b3d3c3476f6c + languageName: node + linkType: hard + "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -2879,6 +2966,30 @@ __metadata: languageName: node linkType: hard +"@open-draft/deferred-promise@npm:^2.2.0": + version: 2.2.0 + resolution: "@open-draft/deferred-promise@npm:2.2.0" + checksum: 10c0/eafc1b1d0fc8edb5e1c753c5e0f3293410b40dde2f92688211a54806d4136887051f39b98c1950370be258483deac9dfd17cf8b96557553765198ef2547e4549 + languageName: node + linkType: hard + +"@open-draft/logger@npm:^0.3.0": + version: 0.3.0 + resolution: "@open-draft/logger@npm:0.3.0" + dependencies: + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.4.0" + checksum: 10c0/90010647b22e9693c16258f4f9adb034824d1771d3baa313057b9a37797f571181005bc50415a934eaf7c891d90ff71dcd7a9d5048b0b6bb438f31bef2c7c5c1 + languageName: node + linkType: hard + +"@open-draft/until@npm:^2.0.0, @open-draft/until@npm:^2.1.0": + version: 2.1.0 + resolution: "@open-draft/until@npm:2.1.0" + checksum: 10c0/61d3f99718dd86bb393fee2d7a785f961dcaf12f2055f0c693b27f4d0cd5f7a03d498a6d9289773b117590d794a43cd129366fd8e99222e4832f67b1653d54cf + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -3651,6 +3762,13 @@ __metadata: languageName: node linkType: hard +"@types/cookie@npm:^0.6.0": + version: 0.6.0 + resolution: "@types/cookie@npm:0.6.0" + checksum: 10c0/5b326bd0188120fb32c0be086b141b1481fec9941b76ad537f9110e10d61ee2636beac145463319c71e4be67a17e85b81ca9e13ceb6e3bb63b93d16824d6c149 + languageName: node + linkType: hard + "@types/doctrine@npm:^0.0.9": version: 0.0.9 resolution: "@types/doctrine@npm:0.0.9" @@ -3837,6 +3955,20 @@ __metadata: languageName: node linkType: hard +"@types/statuses@npm:^2.0.4": + version: 2.0.5 + resolution: "@types/statuses@npm:2.0.5" + checksum: 10c0/4dacec0b29483a44be902a022a11a22b339de7a6e7b2059daa4f7add10cb6dbcc28d02d2a416fe9687e48d335906bf983065391836d4e7c847e55ddef4de8fad + languageName: node + linkType: hard + +"@types/tough-cookie@npm:^4.0.5": + version: 4.0.5 + resolution: "@types/tough-cookie@npm:4.0.5" + checksum: 10c0/68c6921721a3dcb40451543db2174a145ef915bc8bcbe7ad4e59194a0238e776e782b896c7a59f4b93ac6acefca9161fccb31d1ce3b3445cb6faa467297fb473 + languageName: node + linkType: hard + "@types/uuid@npm:^9.0.1": version: 9.0.7 resolution: "@types/uuid@npm:9.0.7" @@ -4328,6 +4460,8 @@ __metadata: file-saver: "npm:^2.0.5" happy-dom: "npm:^15.10.2" history: "npm:^5.3.0" + msw: "npm:^2.6.6" + msw-storybook-addon: "npm:^2.0.4" prop-types: "npm:15.8.1" qs: "npm:^6.10.3" react: "npm:18.3.1" @@ -4349,6 +4483,15 @@ __metadata: languageName: unknown linkType: soft +"ansi-escapes@npm:^4.3.2": + version: 4.3.2 + resolution: "ansi-escapes@npm:4.3.2" + dependencies: + type-fest: "npm:^0.21.3" + checksum: 10c0/da917be01871525a3dfcf925ae2977bc59e8c513d4423368645634bf5d4ceba5401574eb705c1e92b79f7292af5a656f78c5725a4b0e1cec97c4b413705c1d50 + languageName: node + linkType: hard + "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -4874,7 +5017,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.1.0": +"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -4957,6 +5100,24 @@ __metadata: languageName: node linkType: hard +"cli-width@npm:^4.1.0": + version: 4.1.0 + resolution: "cli-width@npm:4.1.0" + checksum: 10c0/1fbd56413578f6117abcaf858903ba1f4ad78370a4032f916745fa2c7e390183a9d9029cf837df320b0fdce8137668e522f60a30a5f3d6529ff3872d265a955f + languageName: node + linkType: hard + +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 10c0/4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5 + languageName: node + linkType: hard + "color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -5026,6 +5187,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^0.7.2": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 + languageName: node + linkType: hard + "core-js-compat@npm:^3.31.0": version: 3.32.1 resolution: "core-js-compat@npm:3.32.1" @@ -6252,6 +6420,13 @@ __metadata: languageName: node linkType: hard +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde + languageName: node + linkType: hard + "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1": version: 1.2.1 resolution: "get-intrinsic@npm:1.2.1" @@ -6406,6 +6581,13 @@ __metadata: languageName: node linkType: hard +"graphql@npm:^16.8.1": + version: 16.9.0 + resolution: "graphql@npm:16.9.0" + checksum: 10c0/a8850f077ff767377237d1f8b1da2ec70aeb7623cdf1dfc9e1c7ae93accc0c8149c85abe68923be9871a2934b1bce5a2496f846d4d56e1cfb03eaaa7ddba9b6a + languageName: node + linkType: hard + "happy-dom@npm:^15.10.2": version: 15.10.2 resolution: "happy-dom@npm:15.10.2" @@ -6479,6 +6661,13 @@ __metadata: languageName: node linkType: hard +"headers-polyfill@npm:^4.0.2": + version: 4.0.3 + resolution: "headers-polyfill@npm:4.0.3" + checksum: 10c0/53e85b2c6385f8d411945fb890c5369f1469ce8aa32a6e8d28196df38568148de640c81cf88cbc7c67767103dd9acba48f4f891982da63178fc6e34560022afe + languageName: node + linkType: hard + "history@npm:^5.3.0": version: 5.3.0 resolution: "history@npm:5.3.0" @@ -6781,6 +6970,13 @@ __metadata: languageName: node linkType: hard +"is-node-process@npm:^1.0.1, is-node-process@npm:^1.2.0": + version: 1.2.0 + resolution: "is-node-process@npm:1.2.0" + checksum: 10c0/5b24fda6776d00e42431d7bcd86bce81cb0b6cabeb944142fe7b077a54ada2e155066ad06dbe790abdb397884bdc3151e04a9707b8cd185099efbc79780573ed + languageName: node + linkType: hard + "is-number-object@npm:^1.0.4": version: 1.0.7 resolution: "is-number-object@npm:1.0.7" @@ -7562,6 +7758,57 @@ __metadata: languageName: node linkType: hard +"msw-storybook-addon@npm:^2.0.4": + version: 2.0.4 + resolution: "msw-storybook-addon@npm:2.0.4" + dependencies: + is-node-process: "npm:^1.0.1" + peerDependencies: + msw: ^2.0.0 + checksum: 10c0/91cd85b0d6e3bdf8cc4f519b29da644d4df324a3fc6753a98b472488986aee888dfe4fc6a216a41057b01d10899e1e3a16ad253f63d7ae06f3dc3560fe5e561d + languageName: node + linkType: hard + +"msw@npm:^2.6.6": + version: 2.6.6 + resolution: "msw@npm:2.6.6" + dependencies: + "@bundled-es-modules/cookie": "npm:^2.0.1" + "@bundled-es-modules/statuses": "npm:^1.0.1" + "@bundled-es-modules/tough-cookie": "npm:^0.1.6" + "@inquirer/confirm": "npm:^5.0.0" + "@mswjs/interceptors": "npm:^0.37.0" + "@open-draft/deferred-promise": "npm:^2.2.0" + "@open-draft/until": "npm:^2.1.0" + "@types/cookie": "npm:^0.6.0" + "@types/statuses": "npm:^2.0.4" + chalk: "npm:^4.1.2" + graphql: "npm:^16.8.1" + headers-polyfill: "npm:^4.0.2" + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.4.3" + path-to-regexp: "npm:^6.3.0" + strict-event-emitter: "npm:^0.5.1" + type-fest: "npm:^4.26.1" + yargs: "npm:^17.7.2" + peerDependencies: + typescript: ">= 4.8.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 10c0/d7f0e0ea6512cfddda5b7695a0c6c6a0ed43b36b62f25925beee8e12daec12a5ef369d00e0976f9dd76d3af779741fda11a3eae812da11a044b03992e697010c + languageName: node + linkType: hard + +"mute-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "mute-stream@npm:2.0.0" + checksum: 10c0/2cf48a2087175c60c8dcdbc619908b49c07f7adcfc37d29236b0c5c612d6204f789104c98cc44d38acab7b3c96f4a3ec2cfdc4934d0738d876dbefa2a12c69f4 + languageName: node + linkType: hard + "nanoid@npm:^3.3.7": version: 3.3.8 resolution: "nanoid@npm:3.3.8" @@ -7766,6 +8013,13 @@ __metadata: languageName: node linkType: hard +"outvariant@npm:^1.4.0, outvariant@npm:^1.4.3": + version: 1.4.3 + resolution: "outvariant@npm:1.4.3" + checksum: 10c0/5976ca7740349cb8c71bd3382e2a762b1aeca6f33dc984d9d896acdf3c61f78c3afcf1bfe9cc633a7b3c4b295ec94d292048f83ea2b2594fae4496656eba992c + languageName: node + linkType: hard + "p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" @@ -7869,6 +8123,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^6.3.0": + version: 6.3.0 + resolution: "path-to-regexp@npm:6.3.0" + checksum: 10c0/73b67f4638b41cde56254e6354e46ae3a2ebc08279583f6af3d96fe4664fc75788f74ed0d18ca44fa4a98491b69434f9eee73b97bb5314bd1b5adb700f5c18d6 + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -7991,6 +8252,15 @@ __metadata: languageName: node linkType: hard +"psl@npm:^1.1.33": + version: 1.15.0 + resolution: "psl@npm:1.15.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10c0/d8d45a99e4ca62ca12ac3c373e63d80d2368d38892daa40cfddaa1eb908be98cd549ac059783ef3a56cfd96d57ae8e2fd9ae53d1378d90d42bc661ff924e102a + languageName: node + linkType: hard + "punycode@npm:^2.1.0": version: 2.3.0 resolution: "punycode@npm:2.3.0" @@ -7998,6 +8268,13 @@ __metadata: languageName: node linkType: hard +"punycode@npm:^2.1.1, punycode@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 + languageName: node + linkType: hard + "qs@npm:^6.10.3": version: 6.11.2 resolution: "qs@npm:6.11.2" @@ -8007,6 +8284,13 @@ __metadata: languageName: node linkType: hard +"querystringify@npm:^2.1.1": + version: 2.2.0 + resolution: "querystringify@npm:2.2.0" + checksum: 10c0/3258bc3dbdf322ff2663619afe5947c7926a6ef5fb78ad7d384602974c467fadfc8272af44f5eb8cddd0d011aae8fabf3a929a8eee4b86edcc0a21e6bd10f9aa + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -8342,6 +8626,20 @@ __metadata: languageName: node linkType: hard +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99 + languageName: node + linkType: hard + +"requires-port@npm:^1.0.0": + version: 1.0.0 + resolution: "requires-port@npm:1.0.0" + checksum: 10c0/b2bfdd09db16c082c4326e573a82c0771daaf7b53b9ce8ad60ea46aa6e30aaf475fe9b164800b89f93b748d2c234d8abff945d2551ba47bf5698e04cd7713267 + languageName: node + linkType: hard + "resize-observer-polyfill@npm:^1.4.2": version: 1.5.1 resolution: "resize-observer-polyfill@npm:1.5.1" @@ -8678,7 +8976,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 @@ -8764,6 +9062,13 @@ __metadata: languageName: node linkType: hard +"statuses@npm:^2.0.1": + version: 2.0.1 + resolution: "statuses@npm:2.0.1" + checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0 + languageName: node + linkType: hard + "std-env@npm:^3.7.0": version: 3.7.0 resolution: "std-env@npm:3.7.0" @@ -8789,6 +9094,13 @@ __metadata: languageName: node linkType: hard +"strict-event-emitter@npm:^0.5.1": + version: 0.5.1 + resolution: "strict-event-emitter@npm:0.5.1" + checksum: 10c0/f5228a6e6b6393c57f52f62e673cfe3be3294b35d6f7842fc24b172ae0a6e6c209fa83241d0e433fc267c503bc2f4ffdbe41a9990ff8ffd5ac425ec0489417f7 + languageName: node + linkType: hard + "string-natural-compare@npm:^3.0.1": version: 3.0.1 resolution: "string-natural-compare@npm:3.0.1" @@ -8796,7 +9108,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -9046,6 +9358,18 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^4.1.4": + version: 4.1.4 + resolution: "tough-cookie@npm:4.1.4" + dependencies: + psl: "npm:^1.1.33" + punycode: "npm:^2.1.1" + universalify: "npm:^0.2.0" + url-parse: "npm:^1.5.3" + checksum: 10c0/aca7ff96054f367d53d1e813e62ceb7dd2eda25d7752058a74d64b7266fd07be75908f3753a32ccf866a2f997604b414cfb1916d6e7f69bc64d9d9939b0d6c45 + languageName: node + linkType: hard + "ts-api-utils@npm:^1.3.0": version: 1.4.0 resolution: "ts-api-utils@npm:1.4.0" @@ -9147,6 +9471,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.21.3": + version: 0.21.3 + resolution: "type-fest@npm:0.21.3" + checksum: 10c0/902bd57bfa30d51d4779b641c2bc403cdf1371fb9c91d3c058b0133694fcfdb817aef07a47f40faf79039eecbaa39ee9d3c532deff244f3a19ce68cea71a61e8 + languageName: node + linkType: hard + "type-fest@npm:^2.19.0": version: 2.19.0 resolution: "type-fest@npm:2.19.0" @@ -9154,6 +9485,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.26.1": + version: 4.30.0 + resolution: "type-fest@npm:4.30.0" + checksum: 10c0/9441fbbc971f92a53d7dfdb0db3f9c71a5a33ac3e021ca605cba8ad0b5c0a1e191cc778b4980c534b098ccb4e3322809100baf763be125510c993c9b8361f60e + languageName: node + linkType: hard + "typed-array-buffer@npm:^1.0.0": version: 1.0.0 resolution: "typed-array-buffer@npm:1.0.0" @@ -9282,6 +9620,13 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^0.2.0": + version: 0.2.0 + resolution: "universalify@npm:0.2.0" + checksum: 10c0/cedbe4d4ca3967edf24c0800cfc161c5a15e240dac28e3ce575c689abc11f2c81ccc6532c8752af3b40f9120fb5e454abecd359e164f4f6aa44c29cd37e194fe + languageName: node + linkType: hard + "universalify@npm:^2.0.0": version: 2.0.0 resolution: "universalify@npm:2.0.0" @@ -9338,6 +9683,16 @@ __metadata: languageName: node linkType: hard +"url-parse@npm:^1.5.3": + version: 1.5.10 + resolution: "url-parse@npm:1.5.10" + dependencies: + querystringify: "npm:^2.1.1" + requires-port: "npm:^1.0.0" + checksum: 10c0/bd5aa9389f896974beb851c112f63b466505a04b4807cea2e5a3b7092f6fbb75316f0491ea84e44f66fed55f1b440df5195d7e3a8203f64fcefa19d182f5be87 + languageName: node + linkType: hard + "use-isomorphic-layout-effect@npm:^1.1.2": version: 1.1.2 resolution: "use-isomorphic-layout-effect@npm:1.1.2" @@ -9637,7 +9992,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -9648,6 +10003,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^6.2.0": + version: 6.2.0 + resolution: "wrap-ansi@npm:6.2.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/baad244e6e33335ea24e86e51868fe6823626e3a3c88d9a6674642afff1d34d9a154c917e74af8d845fd25d170c4ea9cf69a47133c3f3656e1252b3d462d9f6c + languageName: node + linkType: hard + "wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" @@ -9681,6 +10047,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249 + languageName: node + linkType: hard + "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" @@ -9702,6 +10075,28 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 + languageName: node + linkType: hard + +"yargs@npm:^17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: 10c0/ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" @@ -9709,6 +10104,13 @@ __metadata: languageName: node linkType: hard +"yoctocolors-cjs@npm:^2.1.2": + version: 2.1.2 + resolution: "yoctocolors-cjs@npm:2.1.2" + checksum: 10c0/a0e36eb88fea2c7981eab22d1ba45e15d8d268626e6c4143305e2c1628fa17ebfaa40cd306161a8ce04c0a60ee0262058eab12567493d5eb1409780853454c6f + languageName: node + linkType: hard + "zustand@npm:^4.4.7": version: 4.4.7 resolution: "zustand@npm:4.4.7" From 616b25a52939f6f5bb7675664a4c0a190e94e2a7 Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Tue, 17 Dec 2024 14:20:23 +0100 Subject: [PATCH 4/7] Docs: Update navigation headings in mkdocs.yml for clarity (#1433) --- backend/mkdocs.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/mkdocs.yml b/backend/mkdocs.yml index 53d097591..e7d9b653d 100644 --- a/backend/mkdocs.yml +++ b/backend/mkdocs.yml @@ -45,7 +45,7 @@ extra_css: - assets/css/style.css nav: - - MUSCLE: + - Getting started: - 1. Overview of the application: 'index.md' - 2. Start the application: '02_Start_the_application.md' - 3. The admin interface: '03_The_admin_interface.md' @@ -57,9 +57,9 @@ nav: - 9. Playback and player widgets: '09_Playback_and_player_widgets.md' - 10. Create a custom ruleset: '10_Create_custom_ruleset.md' - 11. Exporting result data: '11_Exporting_result_data.md' - - Frontend tools: + - Preview frontend components: - 'Storybook': 'https://amsterdam-music-lab.github.io/MUSCLE/storybook' - - Python code documentation: + - Technical documentation: - Experiment app: - Experiment actions: 'experiment_actions.md' - Experiment admin: 'experiment_admin.md' From 15ba66e559842caddffc47214b3e23cce3b671bd Mon Sep 17 00:00:00 2001 From: Evert-R Date: Tue, 17 Dec 2024 14:28:37 +0100 Subject: [PATCH 5/7] Initialize playlist input for new block --- backend/experiment/static/collapsible_blocks.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/experiment/static/collapsible_blocks.js b/backend/experiment/static/collapsible_blocks.js index 01918c92c..78a50c02d 100644 --- a/backend/experiment/static/collapsible_blocks.js +++ b/backend/experiment/static/collapsible_blocks.js @@ -48,6 +48,9 @@ function initializeBlockForm(blockForm) { }); blockForm.classList.add('collapsed'); + + // Initialize playlist input + django.jQuery('.django-select2').djangoSelect2(); } function toggleBlockVisibility(blockForm) { From 6723326e3f3b30ad19aae014a7d20ffe9fceed85 Mon Sep 17 00:00:00 2001 From: Evert-R Date: Tue, 17 Dec 2024 15:22:46 +0100 Subject: [PATCH 6/7] Add ruleset documentation heading --- backend/mkdocs.yml | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/backend/mkdocs.yml b/backend/mkdocs.yml index e7d9b653d..c8ecd2f92 100644 --- a/backend/mkdocs.yml +++ b/backend/mkdocs.yml @@ -59,49 +59,53 @@ nav: - 11. Exporting result data: '11_Exporting_result_data.md' - Preview frontend components: - 'Storybook': 'https://amsterdam-music-lab.github.io/MUSCLE/storybook' - - Technical documentation: - - Experiment app: - - Experiment actions: 'experiment_actions.md' - - Experiment admin: 'experiment_admin.md' - - Experiment models: 'experiment_models.md' + - Ruleset documentation: + - Data models: + - Experiment: 'experiment_models.md' + - Image: 'image_models.md' + - Participant: 'participant_models.md' + - Question: 'question_models.md' + - Result: 'result_models.md' + - Section: 'section_models.md' + - Session: 'session_models.md' + - Theme: 'theme_models.md' + - Rules, actions & utils: - Experiment rules: 'experiment_rules.md' + - Participant utils: 'participant_utils.md' + - Question utils: 'question_utils.md' + - Experiment actions: 'experiment_actions.md' + - Technical documentation: + - Experiment app: + - Experiment admin: 'experiment_admin.md' - Experiment serializers: 'experiment_serializers.md' - Experiment utils: 'experiment_utils.md' - Experiment views: 'experiment_views.md' - Image app: - - Image admin: 'image_admin.md' - - Image models: 'image_models.md' + - Image admin: 'image_admin.md' - Image serializers: 'image_serializers.md' - Image validators: 'image_validators.md' - Participant app: - Participant admin: 'participant_admin.md' - - Participant models: 'participant_models.md' - - Participant utils: 'participant_utils.md' - Participant views: 'participant_views.md' - Question app: - - Question admin: 'question_admin.md' - - Question models: 'question_models.md' + - Question admin: 'question_admin.md' - Question utils: 'question_utils.md' - Question views: 'question_views.md' - Result app: - - Result admin: 'result_admin.md' - - Result models: 'result_models.md' + - Result admin: 'result_admin.md' - Result score: 'result_score.md' - Result utils: 'result_utils.md' - Result views: 'result_views.md' - Section app: - - Section admin: 'section_admin.md' - - Section models: 'section_models.md' + - Section admin: 'section_admin.md' - Section views: 'section_views.md' - Section utils: 'section_utils.md' - Section validators: 'section_validators.md' - Section views: 'section_views.md' - Session app: - - Session admin: 'session_admin.md' - - Session models: 'session_models.md' + - Session admin: 'session_admin.md' - Session views: 'session_views.md' - Theme app: - - Theme admin: 'theme_admin.md' - - Theme models: 'theme_models.md' + - Theme admin: 'theme_admin.md' - Theme views: 'theme_views.md' - About this document: 'this_doc.md' From 45e3b0e50edf1e9f3b3e40701ea38171ffb59707 Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Wed, 18 Dec 2024 11:34:47 +0100 Subject: [PATCH 7/7] docs: Improve docs & typing for the `Explainer` action in the backend (#1430) --- backend/experiment/actions/explainer.py | 79 +++++++++++++++++-------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/backend/experiment/actions/explainer.py b/backend/experiment/actions/explainer.py index d605dc4c0..e2f9e7333 100644 --- a/backend/experiment/actions/explainer.py +++ b/backend/experiment/actions/explainer.py @@ -1,48 +1,79 @@ +from typing import List, TypedDict, Optional from .base_action import BaseAction +class StepResponse(TypedDict): + number: Optional[int] + description: str + + +class ExplainerResponse(TypedDict): + view: str + instruction: str + button_label: str + steps: List[StepResponse] + timer: Optional[int] + + +class Step(object): + """ + A step in an explainer + + Args: + description (str): Description of the step + number (Optional[int]): Optional number of the step + """ + + def __init__(self, description: str, number: Optional[int] = None): + self.description = description + self.number = number + + def action(self, number=None) -> StepResponse: + """Create an explainer step, with description and optional number""" + return {"number": self.number if self.number else number, "description": self.description} + + class Explainer(BaseAction): """ Provide data for a explainer that explains the experiment steps - Relates to client component: Explainer.js + Relates to client component: Explainer.tsx Explainer view automatically proceeds to the following view after timer (in ms) expires. If timer=None, explainer view will proceed to the next view only after a click of a button. Intro explainers should always have timer=None (i.e. interaction with a browser is required), otherwise the browser will not autoplay the first segment. + + Args: + instruction (str): Instruction for the explainer + steps (List[Step]): List of steps to explain + button_label (Optional[str]): Label for the button that proceeds to the next view + timer (Optional[int]): Timer in ms + step_numbers (Optional[bool]): Show step numbers """ ID = "EXPLAINER" - def __init__(self, instruction, steps, button_label="Let's go!", timer=None, step_numbers=False): + def __init__( + self, + instruction: str, + steps: List[Step], + button_label="Let's go!", + timer: Optional[int] = None, + step_numbers: Optional[bool] = False, + ): self.instruction = instruction self.steps = steps self.button_label = button_label self.timer = timer self.step_numbers = step_numbers - def action(self): - """Get data for explainer action""" + def action(self) -> ExplainerResponse: if self.step_numbers: - serialized_steps = [step.action(index+1) for index, step in enumerate(self.steps)] + serialized_steps = [step.action(index + 1) for index, step in enumerate(self.steps)] else: serialized_steps = [step.action() for step in self.steps] return { - 'view': self.ID, - 'instruction': self.instruction, - 'button_label': self.button_label, - 'steps': serialized_steps, - 'timer': self.timer, - } - - -class Step(object): - - def __init__(self, description, number=None): - self.description = description - self.number = number - - def action(self, number=None): - """Create an explainer step, with description and optional number""" - return { - 'number': self.number if self.number else number, - 'description': self.description + "view": self.ID, + "instruction": self.instruction, + "button_label": self.button_label, + "steps": serialized_steps, + "timer": self.timer, }