Add frontend implementation for chapter highlights, re-enable cypress

test
This commit is contained in:
Ramon Wenger 2024-02-22 15:17:41 +01:00
parent a7df777b0b
commit 22cd8eb57f
7 changed files with 137 additions and 21 deletions

View File

@ -331,14 +331,14 @@ describe('Highlights', () => {
cy.getByDataCy('highlight-mark').should('have.length', 1);
});
it.skip('visits a module and highlights the chapter description', () => {
it('visits a module and highlights the chapter description', () => {
cy.mockGraphqlOps({
operations,
});
cy.visit('/module/my-module');
markText('chapter-intro');
const highlightedText = 'is is somet';
const highlightedText = 'is is some';
// delete doesn't make sense before the highlight exists
cy.getByDataCy('highlight-delete').should('not.exist');

View File

@ -47,6 +47,7 @@ export default {
bookmark: null,
descriptionHiddenFor: [],
titleHiddenFor: [],
highlights: [],
}),
ContentBlockNode: () => ({
contents: [],

View File

@ -13,6 +13,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
const documents = {
"\n fragment ChapterHighlightsFragment on ChapterNode {\n id\n __typename\n highlights {\n ...HighlightParts\n }\n }\n": types.ChapterHighlightsFragmentFragmentDoc,
"\n fragment HighlightParts on HighlightNode {\n id\n contentIndex\n paragraphIndex\n selectionLength\n contentUuid\n startPosition\n color\n note {\n text\n }\n text\n page {\n # only one of them should be necessary, but the client somehow doesn't like just the Node inline fragment\n __typename\n ... on ContentBlockNode {\n id\n }\n ... on InstrumentNode {\n id\n slug\n }\n ... on ModuleNode {\n id\n slug\n }\n ... on ChapterNode {\n id\n slug\n }\n }\n }\n": types.HighlightPartsFragmentDoc,
"\n fragment ContentBlockHighlightsFragment on ContentBlockNode {\n id\n __typename\n highlights {\n ...HighlightParts\n }\n }\n": types.ContentBlockHighlightsFragmentFragmentDoc,
"\n query LanguageQuery {\n me {\n language @client\n }\n }\n ": types.LanguageQueryDocument,
@ -59,6 +60,10 @@ const documents = {
*/
export function graphql(source: string): unknown;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ChapterHighlightsFragment on ChapterNode {\n id\n __typename\n highlights {\n ...HighlightParts\n }\n }\n"): (typeof documents)["\n fragment ChapterHighlightsFragment on ChapterNode {\n id\n __typename\n highlights {\n ...HighlightParts\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@ -369,6 +369,7 @@ export type ChapterNode = ChapterInterface & Node & {
contentBlocks?: Maybe<Array<Maybe<ContentBlockNode>>>;
description?: Maybe<Scalars['String']['output']>;
descriptionHiddenFor?: Maybe<Array<Maybe<SchoolClassNode>>>;
highlights?: Maybe<Array<Maybe<HighlightNode>>>;
/** The ID of the object */
id: Scalars['ID']['output'];
path?: Maybe<Scalars['String']['output']>;
@ -2691,6 +2692,11 @@ export type UserGroupBlockVisibility = {
schoolClassId: Scalars['ID']['input'];
};
export type ChapterHighlightsFragmentFragment = { __typename: 'ChapterNode', id: string, highlights?: Array<(
{ __typename?: 'HighlightNode' }
& { ' $fragmentRefs'?: { 'HighlightPartsFragment': HighlightPartsFragment } }
) | null> | null } & { ' $fragmentName'?: 'ChapterHighlightsFragmentFragment' };
export type HighlightPartsFragment = { __typename?: 'HighlightNode', id: string, contentIndex?: number | null, paragraphIndex: number, selectionLength: number, contentUuid?: any | null, startPosition: number, color: string, text: string, note?: { __typename?: 'NoteNode', text: string } | null, page?: { __typename: 'ChapterNode', id: string, slug: string } | { __typename: 'ContentBlockNode', id: string } | { __typename: 'InstrumentNode', id: string, slug: string } | { __typename: 'ModuleNode', id: string, slug: string } | null } & { ' $fragmentName'?: 'HighlightPartsFragment' };
export type ContentBlockHighlightsFragmentFragment = { __typename: 'ContentBlockNode', id: string, highlights?: Array<(
@ -2872,6 +2878,7 @@ export type ModuleSolutionsQueryVariables = Exact<{
export type ModuleSolutionsQuery = { __typename?: 'Query', module?: { __typename?: 'ModuleNode', solutionsEnabled?: boolean | null, slug: string } | null };
export const HighlightPartsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"HighlightParts"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HighlightNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contentIndex"}},{"kind":"Field","name":{"kind":"Name","value":"paragraphIndex"}},{"kind":"Field","name":{"kind":"Name","value":"selectionLength"}},{"kind":"Field","name":{"kind":"Name","value":"contentUuid"}},{"kind":"Field","name":{"kind":"Name","value":"startPosition"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"note"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"page"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContentBlockNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InstrumentNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ChapterNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode<HighlightPartsFragment, unknown>;
export const ChapterHighlightsFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ChapterHighlightsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ChapterNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"highlights"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"HighlightParts"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"HighlightParts"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HighlightNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contentIndex"}},{"kind":"Field","name":{"kind":"Name","value":"paragraphIndex"}},{"kind":"Field","name":{"kind":"Name","value":"selectionLength"}},{"kind":"Field","name":{"kind":"Name","value":"contentUuid"}},{"kind":"Field","name":{"kind":"Name","value":"startPosition"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"note"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"page"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContentBlockNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InstrumentNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ChapterNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode<ChapterHighlightsFragmentFragment, unknown>;
export const ContentBlockHighlightsFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ContentBlockHighlightsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContentBlockNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"highlights"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"HighlightParts"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"HighlightParts"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HighlightNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contentIndex"}},{"kind":"Field","name":{"kind":"Name","value":"paragraphIndex"}},{"kind":"Field","name":{"kind":"Name","value":"selectionLength"}},{"kind":"Field","name":{"kind":"Name","value":"contentUuid"}},{"kind":"Field","name":{"kind":"Name","value":"startPosition"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"note"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"page"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContentBlockNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InstrumentNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ChapterNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode<ContentBlockHighlightsFragmentFragment, unknown>;
export const ModuleHighlightsFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ModuleHighlightsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"highlights"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"HighlightParts"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"HighlightParts"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"HighlightNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contentIndex"}},{"kind":"Field","name":{"kind":"Name","value":"paragraphIndex"}},{"kind":"Field","name":{"kind":"Name","value":"selectionLength"}},{"kind":"Field","name":{"kind":"Name","value":"contentUuid"}},{"kind":"Field","name":{"kind":"Name","value":"startPosition"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"note"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"page"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContentBlockNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InstrumentNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ChapterNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode<ModuleHighlightsFragmentFragment, unknown>;
export const ModuleLevelFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ModuleLevelFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ModuleLevelNode"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"filterAttributeType"}}]}}]} as unknown as DocumentNode<ModuleLevelFragmentFragment, unknown>;

View File

@ -40,6 +40,7 @@
class="chapter__intro intro hideable-element"
data-cy="chapter-intro"
v-if="!descriptionHidden"
ref="chapterIntro"
>
<visibility-action
:block="chapter"
@ -88,9 +89,23 @@ import { getMe } from '@/mixins/me';
import { useMutation } from '@vue/apollo-composable';
import { useRoute } from 'vue-router';
import { PAGE_LOAD_TIMEOUT } from '@/consts/navigation.consts';
import {
SelectionHandlerOptions,
SelectionHandlerType,
createHighlightCurry,
getSelectionHandler,
markHighlight,
} from '@/helpers/highlight';
import { onUnmounted } from 'vue';
import { AddHighlightArgument, ChapterNode, HighlightNode } from '@/__generated__/graphql';
import { graphql } from '@/__generated__';
import highlightSidebar from '@/helpers/highlight-sidebar';
import { doUpdateHighlight } from '@/graphql/mutations';
import { watch } from 'vue';
import Mark from 'mark.js';
export interface Props {
chapter: Chapter;
chapter: ChapterNode;
index: number;
editMode: boolean;
}
@ -100,6 +115,39 @@ const { schoolClass } = getMe();
const store = useStore();
const route = useRoute();
const wrapper = ref<HTMLElement | null>(null);
const chapterIntro = ref<HTMLElement | null>(null);
const props = withDefaults(defineProps<Props>(), {
index: 0,
editMode: false,
});
const chapterHighlightsFragment = graphql(`
fragment ChapterHighlightsFragment on ChapterNode {
id
__typename
highlights {
...HighlightParts
}
}
`);
const createHighlight = createHighlightCurry({
fragment: chapterHighlightsFragment,
fragmentName: 'ChapterHighlightsFragment',
cacheSignature: { id: props.chapter.id, __typename: 'ChapterNode' },
isContentHighlight: false,
});
const markHighlights = () => {
if (props.chapter.highlights && chapterIntro.value) {
for (const highlight of props.chapter.highlights) {
const highlightNode = highlight as HighlightNode;
const element = chapterIntro.value.children[highlightNode.paragraphIndex] as HTMLElement;
markHighlight(highlightNode, element, element);
}
}
};
let selectionHandler: SelectionHandlerType;
onMounted(() => {
const element = wrapper.value;
@ -111,11 +159,46 @@ onMounted(() => {
}, PAGE_LOAD_TIMEOUT);
}
}
const intro = chapterIntro.value;
if (intro !== null) {
const options: SelectionHandlerOptions = {
el: intro,
page: props.chapter,
parentSelector: 'chapter__intro',
onChangeColor: (newHighlight: AddHighlightArgument) => {
createHighlight(newHighlight);
},
onCreateNote: async (newHighlight: AddHighlightArgument) => {
// we also open the sidebar when clicking on the note icon
const highlight = await createHighlight(newHighlight);
highlightSidebar.open({
highlight,
onUpdateText: (text: string) => {
doUpdateHighlight({
input: {
note: text,
id: highlight.id,
},
});
},
});
},
};
selectionHandler = getSelectionHandler(options);
intro.addEventListener('mouseup', selectionHandler);
}
markHighlights();
});
const props = withDefaults(defineProps<Props>(), {
index: 0,
editMode: false,
onUnmounted(() => {
const intro = chapterIntro.value;
if (intro !== null) {
intro.removeEventListener('mouseup', selectionHandler);
}
});
const filteredContentBlocks = computed(() => {
@ -230,9 +313,26 @@ const textHidden = (type: string) => {
type,
});
};
</script>
<script></script>
const unmark = () => {
for (const paragraph of chapterIntro.value.children) {
const instance = new Mark(paragraph);
instance.unmark();
}
};
const highlights = computed(() => {
return props.chapter ? props.chapter.highlights : [];
});
watch(
() => highlights.value?.filter((h) => h.color),
() => {
unmark();
markHighlights();
}
);
</script>
<style lang="scss">
/* todo: re-add `scoped`, same as in ContentBlock.vue */

View File

@ -154,9 +154,9 @@ onMounted(() => {
onChangeColor: (newHighlight: AddHighlightArgument) => {
createHighlight(newHighlight);
},
onCreateNote: (newHighlight: AddHighlightArgument) => {
onCreateNote: async (newHighlight: AddHighlightArgument) => {
// we also open the sidebar when clicking on the note icon
createHighlight(newHighlight).then((highlight) => {
const highlight = await createHighlight(newHighlight);
highlightSidebar.open({
highlight,
onUpdateText: (text: string) => {
@ -168,7 +168,6 @@ onMounted(() => {
});
},
});
});
},
};

View File

@ -1,3 +1,4 @@
#import "./highlightParts.gql"
fragment ChapterParts on ChapterNode {
id
title
@ -16,4 +17,7 @@ fragment ChapterParts on ChapterNode {
id
name
}
highlights {
...HighlightLegacyParts
}
}