Add copy link button to chapters, also generate redirect link
This commit is contained in:
parent
370afd0b0c
commit
a629f6a5e6
|
|
@ -9,8 +9,12 @@
|
||||||
class="hideable-element"
|
class="hideable-element"
|
||||||
v-if="!titleHidden"
|
v-if="!titleHidden"
|
||||||
>
|
>
|
||||||
<h3 :id="'chapter-' + index">
|
<h3
|
||||||
|
class="chapter__title"
|
||||||
|
:id="'chapter-' + index"
|
||||||
|
>
|
||||||
{{ chapter.title }}
|
{{ chapter.title }}
|
||||||
|
<copy-link :id="chapter.id" />
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -66,6 +70,7 @@ import ContentBlock from '@/components/ContentBlock';
|
||||||
import AddContentButton from '@/components/AddContentButton';
|
import AddContentButton from '@/components/AddContentButton';
|
||||||
import BookmarkActions from '@/components/notes/BookmarkActions';
|
import BookmarkActions from '@/components/notes/BookmarkActions';
|
||||||
import VisibilityAction from '@/components/visibility/VisibilityAction';
|
import VisibilityAction from '@/components/visibility/VisibilityAction';
|
||||||
|
import CopyLink from '@/components/CopyLink.vue';
|
||||||
|
|
||||||
import { hidden } from '@/helpers/visibility';
|
import { hidden } from '@/helpers/visibility';
|
||||||
import { CHAPTER_DESCRIPTION_TYPE, CHAPTER_TITLE_TYPE, CONTENT_TYPE } from '@/consts/types';
|
import { CHAPTER_DESCRIPTION_TYPE, CHAPTER_TITLE_TYPE, CONTENT_TYPE } from '@/consts/types';
|
||||||
|
|
@ -98,6 +103,7 @@ export default {
|
||||||
VisibilityAction,
|
VisibilityAction,
|
||||||
ContentBlock,
|
ContentBlock,
|
||||||
AddContentButton,
|
AddContentButton,
|
||||||
|
CopyLink,
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -231,6 +237,12 @@ export default {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
&__description {
|
&__description {
|
||||||
@include lead-paragraph;
|
@include lead-paragraph;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -219,10 +219,7 @@ onMounted(() => {
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
window.scrollTo({ top: rect.y, behavior: 'smooth' });
|
window.scrollTo({ top: rect.y, behavior: 'smooth' });
|
||||||
top.value = rect.y;
|
top.value = rect.y;
|
||||||
console.log(rect.y);
|
|
||||||
console.log(rect);
|
|
||||||
}, 750);
|
}, 750);
|
||||||
console.log(document.readyState);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, computed, ref } from 'vue';
|
import { defineAsyncComponent, computed, ref } from 'vue';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
const LinkIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/LinkIcon.vue'));
|
const LinkIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */ '@/components/icons/LinkIcon.vue'));
|
||||||
|
|
||||||
|
|
@ -26,14 +27,9 @@ const show = ref(false);
|
||||||
|
|
||||||
const copyLink = () => {
|
const copyLink = () => {
|
||||||
show.value = true;
|
show.value = true;
|
||||||
navigator.clipboard.writeText(directLink.value).then(
|
navigator.clipboard.writeText(directLink.value).then(() => {
|
||||||
() => {
|
log.debug('copied link', directLink.value);
|
||||||
console.log('yay!', directLink.value);
|
});
|
||||||
},
|
|
||||||
() => {
|
|
||||||
console.log('nay!');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
show.value = false;
|
show.value = false;
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
<template>
|
<template><div /></template>
|
||||||
<div>
|
|
||||||
<h1>ContentBlockLocator {{ props.id }}</h1>
|
|
||||||
<pre>{{ result }}</pre>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useQuery } from '@vue/apollo-composable';
|
import { useQuery } from '@vue/apollo-composable';
|
||||||
|
|
@ -14,22 +9,37 @@ const props = defineProps<{
|
||||||
id: string;
|
id: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { result, onResult } = useQuery(gql`
|
const decoded = window.atob(props.id);
|
||||||
query {
|
|
||||||
contentBlock(id: "${props.id}") {
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
onResult(
|
let query;
|
||||||
({
|
|
||||||
data: {
|
if (decoded.startsWith('ChapterNode')) {
|
||||||
contentBlock: { path },
|
query = gql`
|
||||||
},
|
query ChapterQuery($id: ID!) {
|
||||||
}) => {
|
chapter(id: $id) {
|
||||||
// console.log(queryResult);
|
path
|
||||||
router.push(`/${path}`);
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
query = gql`
|
||||||
|
query ContentBlockQuery($id: ID!) {
|
||||||
|
contentBlock(id: $id) {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onResult } = useQuery(query, () => ({ id: props.id }));
|
||||||
|
|
||||||
|
onResult(({ data }) => {
|
||||||
|
let path;
|
||||||
|
try {
|
||||||
|
path = data.chapter.path;
|
||||||
|
} catch (e) {
|
||||||
|
path = data.contentBlock.path;
|
||||||
}
|
}
|
||||||
);
|
router.push(`/${path}`);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -5,51 +5,67 @@ from wagtail.admin.edit_handlers import FieldPanel, TabbedInterface, ObjectList
|
||||||
|
|
||||||
from core.wagtail_utils import StrictHierarchyPage, get_default_settings
|
from core.wagtail_utils import StrictHierarchyPage, get_default_settings
|
||||||
from users.models import SchoolClass
|
from users.models import SchoolClass
|
||||||
|
from core.mixins import GraphqlNodeMixin
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Chapter(StrictHierarchyPage):
|
class Chapter(StrictHierarchyPage, GraphqlNodeMixin):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Kapitel'
|
verbose_name = "Kapitel"
|
||||||
verbose_name_plural = 'Kapitel'
|
verbose_name_plural = "Kapitel"
|
||||||
|
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
|
|
||||||
content_panels = [
|
content_panels = [
|
||||||
FieldPanel('title', classname="full title"),
|
FieldPanel("title", classname="full title"),
|
||||||
FieldPanel('description', classname="full description"),
|
FieldPanel("description", classname="full description"),
|
||||||
]
|
]
|
||||||
|
|
||||||
edit_handler = TabbedInterface([
|
edit_handler = TabbedInterface(
|
||||||
ObjectList(content_panels, heading='Content'),
|
[ObjectList(content_panels, heading="Content"), get_default_settings()]
|
||||||
get_default_settings()
|
)
|
||||||
])
|
|
||||||
|
|
||||||
template = 'generic_page.html'
|
template = "generic_page.html"
|
||||||
|
|
||||||
parent_page_types = ['books.Module']
|
parent_page_types = ["books.Module"]
|
||||||
subpage_types = ['books.ContentBlock']
|
subpage_types = ["books.ContentBlock"]
|
||||||
|
|
||||||
title_hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_chapter_titles')
|
title_hidden_for = models.ManyToManyField(
|
||||||
description_hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_chapter_descriptions')
|
SchoolClass, related_name="hidden_chapter_titles"
|
||||||
|
)
|
||||||
|
description_hidden_for = models.ManyToManyField(
|
||||||
|
SchoolClass, related_name="hidden_chapter_descriptions"
|
||||||
|
)
|
||||||
|
|
||||||
def sync_title_visibility(self, school_class_template, school_class_to_sync):
|
def sync_title_visibility(self, school_class_template, school_class_to_sync):
|
||||||
if self.title_hidden_for.filter(id=school_class_template.id).exists() \
|
if (
|
||||||
and not self.title_hidden_for.filter(id=school_class_to_sync.id).exists():
|
self.title_hidden_for.filter(id=school_class_template.id).exists()
|
||||||
|
and not self.title_hidden_for.filter(id=school_class_to_sync.id).exists()
|
||||||
|
):
|
||||||
self.title_hidden_for.add(school_class_to_sync)
|
self.title_hidden_for.add(school_class_to_sync)
|
||||||
|
|
||||||
if self.title_hidden_for.filter(
|
if (
|
||||||
id=school_class_to_sync.id).exists() \
|
self.title_hidden_for.filter(id=school_class_to_sync.id).exists()
|
||||||
and not self.title_hidden_for.filter(id=school_class_template.id).exists():
|
and not self.title_hidden_for.filter(id=school_class_template.id).exists()
|
||||||
|
):
|
||||||
self.title_hidden_for.remove(school_class_to_sync)
|
self.title_hidden_for.remove(school_class_to_sync)
|
||||||
|
|
||||||
def sync_description_visibility(self, school_class_template, school_class_to_sync):
|
def sync_description_visibility(self, school_class_template, school_class_to_sync):
|
||||||
if self.description_hidden_for.filter(id=school_class_template.id).exists() \
|
if (
|
||||||
and not self.description_hidden_for.filter(id=school_class_to_sync.id).exists():
|
self.description_hidden_for.filter(
|
||||||
|
id=school_class_template.id).exists()
|
||||||
|
and not self.description_hidden_for.filter(
|
||||||
|
id=school_class_to_sync.id
|
||||||
|
).exists()
|
||||||
|
):
|
||||||
self.description_hidden_for.add(school_class_to_sync)
|
self.description_hidden_for.add(school_class_to_sync)
|
||||||
|
|
||||||
if self.description_hidden_for.filter(
|
if (
|
||||||
id=school_class_to_sync.id).exists() \
|
self.description_hidden_for.filter(
|
||||||
and not self.description_hidden_for.filter(id=school_class_template.id).exists():
|
id=school_class_to_sync.id).exists()
|
||||||
|
and not self.description_hidden_for.filter(
|
||||||
|
id=school_class_template.id
|
||||||
|
).exists()
|
||||||
|
):
|
||||||
self.description_hidden_for.remove(school_class_to_sync)
|
self.description_hidden_for.remove(school_class_to_sync)
|
||||||
|
|
|
||||||
|
|
@ -11,43 +11,55 @@ from notes.schema import ChapterBookmarkNode
|
||||||
|
|
||||||
class ChapterNode(DjangoObjectType):
|
class ChapterNode(DjangoObjectType):
|
||||||
bookmark = graphene.Field(ChapterBookmarkNode)
|
bookmark = graphene.Field(ChapterBookmarkNode)
|
||||||
content_blocks = graphene.List('books.schema.nodes.ContentBlockNode')
|
content_blocks = graphene.List("books.schema.nodes.ContentBlockNode")
|
||||||
title_hidden_for = graphene.List('users.schema.SchoolClassNode')
|
title_hidden_for = graphene.List("users.schema.SchoolClassNode")
|
||||||
description_hidden_for = graphene.List('users.schema.SchoolClassNode')
|
description_hidden_for = graphene.List("users.schema.SchoolClassNode")
|
||||||
|
path = graphene.String()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Chapter
|
model = Chapter
|
||||||
only_fields = [
|
only_fields = [
|
||||||
'slug', 'title', 'description', 'title_hidden_for', 'description_hidden_for'
|
"slug",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"title_hidden_for",
|
||||||
|
"description_hidden_for",
|
||||||
]
|
]
|
||||||
filter_fields = [
|
filter_fields = [
|
||||||
'slug', 'title',
|
"slug",
|
||||||
|
"title",
|
||||||
]
|
]
|
||||||
interfaces = (relay.Node, ChapterInterface)
|
interfaces = (relay.Node, ChapterInterface)
|
||||||
|
|
||||||
def resolve_content_blocks(self, info, **kwargs):
|
def resolve_content_blocks(self, info, **kwargs):
|
||||||
user = info.context.user
|
user = info.context.user
|
||||||
school_classes = user.school_classes.values_list('pk', flat=True)
|
school_classes = user.school_classes.values_list("pk", flat=True)
|
||||||
|
|
||||||
by_parent = ContentBlock.get_by_parent(self) \
|
by_parent = ContentBlock.get_by_parent(self).filter(
|
||||||
.filter(contentblocksnapshot__isnull=True) # exclude snapshot
|
contentblocksnapshot__isnull=True
|
||||||
|
) # exclude snapshot
|
||||||
# .prefetch_related('visible_for') \
|
# .prefetch_related('visible_for') \
|
||||||
# .prefetch_related('hidden_for')
|
# .prefetch_related('hidden_for')
|
||||||
|
|
||||||
# don't filter the hidden blocks on the server any more, we do this on the client now, as they are not secret
|
# don't filter the hidden blocks on the server any more, we do this on the client now, as they are not secret
|
||||||
default_blocks = Q(user_created=False)
|
default_blocks = Q(user_created=False)
|
||||||
owned_by_user = Q(user_created=True, owner=user)
|
owned_by_user = Q(user_created=True, owner=user)
|
||||||
teacher_created_and_visible = Q(Q(user_created=True) & Q(visible_for__pk__in=school_classes))
|
teacher_created_and_visible = Q(
|
||||||
|
Q(user_created=True) & Q(visible_for__pk__in=school_classes)
|
||||||
|
)
|
||||||
|
|
||||||
if user.can_manage_school_class_content: # teacher
|
if user.can_manage_school_class_content: # teacher
|
||||||
return by_parent.filter(default_blocks | owned_by_user | teacher_created_and_visible).distinct()
|
return by_parent.filter(
|
||||||
|
default_blocks | owned_by_user | teacher_created_and_visible
|
||||||
|
).distinct()
|
||||||
else: # student
|
else: # student
|
||||||
return by_parent.filter(default_blocks | teacher_created_and_visible).distinct()
|
return by_parent.filter(
|
||||||
|
default_blocks | teacher_created_and_visible
|
||||||
|
).distinct()
|
||||||
|
|
||||||
def resolve_bookmark(self, info, **kwargs):
|
def resolve_bookmark(self, info, **kwargs):
|
||||||
return ChapterBookmark.objects.filter(
|
return ChapterBookmark.objects.filter(
|
||||||
user=info.context.user,
|
user=info.context.user, chapter=self
|
||||||
chapter=self
|
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -57,3 +69,8 @@ class ChapterNode(DjangoObjectType):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_description_hidden_for(parent: Chapter, info, **kwargs):
|
def resolve_description_hidden_for(parent: Chapter, info, **kwargs):
|
||||||
return parent.description_hidden_for.all()
|
return parent.description_hidden_for.all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_path(root: Chapter, info, **kwargs):
|
||||||
|
module = root.get_parent()
|
||||||
|
return f"module/{module.slug}#{root.graphql_id}"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue