Merged in feature/content-block-instrument-type-MS-480 (pull request #119)
Feature/content block instrument type MS-480 Approved-by: Christian Cueni
This commit is contained in:
commit
6476d09f6d
|
|
@ -26,7 +26,11 @@ describe('Instruments on Module page', () => {
|
||||||
title: 'Some Chapter',
|
title: 'Some Chapter',
|
||||||
contentBlocks: [
|
contentBlocks: [
|
||||||
{
|
{
|
||||||
'type': 'base_communication',
|
'type': 'instrument',
|
||||||
|
instrumentCategory: {
|
||||||
|
id: 'category-id',
|
||||||
|
name: 'Sprache & Kommunikation'
|
||||||
|
},
|
||||||
'title': 'Das Interview',
|
'title': 'Das Interview',
|
||||||
'contents': [
|
'contents': [
|
||||||
{
|
{
|
||||||
|
|
@ -40,6 +44,7 @@ describe('Instruments on Module page', () => {
|
||||||
{
|
{
|
||||||
'type': 'normal',
|
'type': 'normal',
|
||||||
'title': 'Normaler Block',
|
'title': 'Normaler Block',
|
||||||
|
instrumentCategory: null,
|
||||||
'contents': [
|
'contents': [
|
||||||
{
|
{
|
||||||
type: 'text_block',
|
type: 'text_block',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="specialClass"
|
:class="specialClass"
|
||||||
|
:style="instrumentStyle"
|
||||||
class="content-block"
|
class="content-block"
|
||||||
data-cy="content-block"
|
data-cy="content-block"
|
||||||
>
|
>
|
||||||
|
|
@ -43,6 +44,7 @@
|
||||||
<h3
|
<h3
|
||||||
class="content-block__instrument-label"
|
class="content-block__instrument-label"
|
||||||
data-cy="instrument-label"
|
data-cy="instrument-label"
|
||||||
|
:style="instrumentLabelStyle"
|
||||||
v-if="instrumentLabel !== ''"
|
v-if="instrumentLabel !== ''"
|
||||||
>
|
>
|
||||||
{{ instrumentLabel }}
|
{{ instrumentLabel }}
|
||||||
|
|
@ -129,13 +131,37 @@
|
||||||
specialClass() {
|
specialClass() {
|
||||||
return `content-block--${this.contentBlock.type.toLowerCase()}`;
|
return `content-block--${this.contentBlock.type.toLowerCase()}`;
|
||||||
},
|
},
|
||||||
|
isInstrumentBlock() {
|
||||||
|
return !!this.contentBlock.instrumentCategory;
|
||||||
|
},
|
||||||
|
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
|
||||||
|
instrumentStyle() {
|
||||||
|
if (this.isInstrumentBlock) {
|
||||||
|
return {
|
||||||
|
backgroundColor: this.contentBlock.instrumentCategory.background
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
instrumentLabel() {
|
instrumentLabel() {
|
||||||
const contentType = this.contentBlock.type.toLowerCase();
|
const contentType = this.contentBlock.type.toLowerCase();
|
||||||
if (contentType.startsWith('base')) { // all instruments start with `base`
|
if (contentType.startsWith('base')) { // all legacy instruments start with `base`
|
||||||
return instrumentCategory(contentType);
|
return instrumentCategory(contentType);
|
||||||
}
|
}
|
||||||
|
if (this.isInstrumentBlock) {
|
||||||
|
return instrumentCategory(this.contentBlock.instrumentCategory.name);
|
||||||
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
|
||||||
|
instrumentLabelStyle() {
|
||||||
|
if (this.isInstrumentBlock) {
|
||||||
|
return {
|
||||||
|
color: this.contentBlock.instrumentCategory.foreground
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
canEditContentBlock() {
|
canEditContentBlock() {
|
||||||
return this.contentBlock.mine && !this.contentBlock.indent;
|
return this.contentBlock.mine && !this.contentBlock.indent;
|
||||||
},
|
},
|
||||||
|
|
@ -316,6 +342,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--instrument {
|
||||||
|
@include content-box-base;
|
||||||
|
}
|
||||||
|
|
||||||
/deep/ p {
|
/deep/ p {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@
|
||||||
<router-link
|
<router-link
|
||||||
:to="{name: 'instrument', params: { slug: value.slug }}"
|
:to="{name: 'instrument', params: { slug: value.slug }}"
|
||||||
class="instrument-widget__button button"
|
class="instrument-widget__button button"
|
||||||
|
:style="{
|
||||||
|
borderColor: value.foreground
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
{{ $flavor.textInstrument }} anzeigen
|
{{ $flavor.textInstrument }} anzeigen
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
@ -15,6 +18,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
|
||||||
export default {
|
export default {
|
||||||
props: ['value'],
|
props: ['value'],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,17 @@
|
||||||
<a
|
<a
|
||||||
:class="typeClass"
|
:class="typeClass"
|
||||||
class="filter-entry"
|
class="filter-entry"
|
||||||
|
:style="categoryStyle"
|
||||||
>
|
>
|
||||||
<span class="filter-entry__text">{{ text }}</span>
|
<span class="filter-entry__text">{{ text }}</span>
|
||||||
<span class="filter-entry__icon-wrapper">
|
<span
|
||||||
<chevron-right class="filter-entry__icon" />
|
:style="activeStyle"
|
||||||
|
class="filter-entry__icon-wrapper"
|
||||||
|
>
|
||||||
|
<chevron-right
|
||||||
|
:style="{fill: category.foreground}"
|
||||||
|
class="filter-entry__icon"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -13,6 +20,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql';
|
import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql';
|
||||||
|
|
||||||
const ChevronRight = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronRight');
|
const ChevronRight = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronRight');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -31,7 +39,7 @@
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -41,8 +49,8 @@
|
||||||
|
|
||||||
apollo: {
|
apollo: {
|
||||||
instrumentFilter: {
|
instrumentFilter: {
|
||||||
query: INSTRUMENT_FILTER_QUERY
|
query: INSTRUMENT_FILTER_QUERY,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -56,12 +64,31 @@
|
||||||
computed: {
|
computed: {
|
||||||
isActive() {
|
isActive() {
|
||||||
if (!this.instrumentFilter.currentFilter) {
|
if (!this.instrumentFilter.currentFilter) {
|
||||||
return this.type === '';
|
return this.id === '';
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const [_, identifier] = this.instrumentFilter.currentFilter.split(':');
|
const [_, identifier] = this.instrumentFilter.currentFilter.split(':');
|
||||||
return this.type === identifier;
|
return this.id === identifier;
|
||||||
},
|
},
|
||||||
|
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
|
||||||
|
activeStyle() {
|
||||||
|
if (this.isActive) {
|
||||||
|
return {
|
||||||
|
backgroundColor: this.category.background,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
|
||||||
|
categoryStyle() {
|
||||||
|
if (this.isCategory) {
|
||||||
|
return {
|
||||||
|
color: this.category.foreground,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
|
||||||
typeClass() {
|
typeClass() {
|
||||||
return {
|
return {
|
||||||
'filter-entry--active': this.isActive,
|
'filter-entry--active': this.isActive,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
import {INTERDISCIPLINARY, LANGUAGE_COMMUNICATION, SOCIETY} from '@/consts/instrument.consts';
|
import {INTERDISCIPLINARY, LANGUAGE_COMMUNICATION, SOCIETY} from '@/consts/instrument.consts';
|
||||||
import {instrumentCategory} from '@/helpers/instrumentType';
|
import {instrumentCategory} from '@/helpers/instrumentType';
|
||||||
|
|
||||||
|
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
instrument: {
|
instrument: {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@ fragment ContentBlockParts on ContentBlockNode {
|
||||||
slug
|
slug
|
||||||
userCreated
|
userCreated
|
||||||
mine
|
mine
|
||||||
|
instrumentCategory {
|
||||||
|
id
|
||||||
|
foreground
|
||||||
|
background
|
||||||
|
name
|
||||||
|
}
|
||||||
bookmarks {
|
bookmarks {
|
||||||
uuid
|
uuid
|
||||||
note {
|
note {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ query InstrumentCategoriesQuery {
|
||||||
instrumentCategories {
|
instrumentCategories {
|
||||||
name
|
name
|
||||||
id
|
id
|
||||||
|
foreground
|
||||||
|
background
|
||||||
types {
|
types {
|
||||||
name
|
name
|
||||||
id
|
id
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const instrumentType = (instrument) => {
|
||||||
} else {
|
} else {
|
||||||
return instrument.type.category.name;
|
return instrument.type.category.name;
|
||||||
}
|
}
|
||||||
return typeDictionary[category] || '';
|
return typeDictionary[category] || category || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const instrumentCategory = (instrument) => {
|
const instrumentCategory = (instrument) => {
|
||||||
|
|
|
||||||
|
|
@ -61,18 +61,25 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin content-box($color-list) {
|
@mixin content-box-base {
|
||||||
background-color: nth($color-list, 2);
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
border-radius: $default-border-radius;
|
border-radius: $default-border-radius;
|
||||||
|
|
||||||
/deep/ .button {
|
/deep/ .button {
|
||||||
border-color: nth($color-list, 1);
|
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin content-box($color-list) {
|
||||||
|
@include content-box-base;
|
||||||
|
background-color: nth($color-list, 2);
|
||||||
|
|
||||||
|
/deep/ .button {
|
||||||
|
border-color: nth($color-list, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@mixin desktop {
|
@mixin desktop {
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1200px) {
|
||||||
@content
|
@content
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,10 @@ def augment_fields(raw_data):
|
||||||
logger.error('Survey {} does not exist'.format(survey_id))
|
logger.error('Survey {} does not exist'.format(survey_id))
|
||||||
if _type == 'basic_knowledge' or _type == 'instrument':
|
if _type == 'basic_knowledge' or _type == 'instrument':
|
||||||
_value = data['value']
|
_value = data['value']
|
||||||
basic_knowledge = BasicKnowledge.objects.get(pk=_value['basic_knowledge'])
|
instrument = BasicKnowledge.objects.get(pk=_value['basic_knowledge'])
|
||||||
_value.update({
|
_value.update({
|
||||||
'slug': basic_knowledge.slug
|
'slug': instrument.slug,
|
||||||
|
'foreground': instrument.new_type.category.foreground
|
||||||
})
|
})
|
||||||
data['value'] = _value
|
data['value'] = _value
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ class ContentBlockFactory(BasePageFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContentBlock
|
model = ContentBlock
|
||||||
|
|
||||||
type = factory.LazyAttribute(lambda x: random.choice(['normal', 'base_communication', 'task', 'base_society']))
|
type = factory.LazyAttribute(lambda x: random.choice(['normal', 'instrument', 'task',]))
|
||||||
|
|
||||||
contents = wagtail_factories.StreamFieldFactory({
|
contents = wagtail_factories.StreamFieldFactory({
|
||||||
'text_block': TextBlockFactory,
|
'text_block': TextBlockFactory,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.13 on 2022-09-15 13:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('books', '0036_alter_contentblock_contents'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='contentblock',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('normal', 'Normal'), ('base_communication', 'Instrument Sprache & Kommunikation'), ('task', 'Auftrag'), ('instrument', 'Instrument'), ('base_society', 'Instrument Gesellschaft'), ('base_interdisciplinary', 'Überfachliches Instrument')], default='normal', max_length=100),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.13 on 2022-09-15 13:40
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
def migrate_instruments(apps, schema_editor):
|
||||||
|
ContentBlock = apps.get_model('books', 'ContentBlock')
|
||||||
|
ContentBlock.objects.filter(type__startswith='base_').update(type='instrument')
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('books', '0037_alter_contentblock_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_instruments, migrations.RunPython.noop)
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.13 on 2022-09-15 14:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('books', '0038_auto_20220915_1340'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='contentblock',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('normal', 'Normal'), ('task', 'Auftrag'), ('instrument', 'Instrument')], default='normal', max_length=100),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -26,17 +26,13 @@ class ContentBlock(StrictHierarchyPage):
|
||||||
verbose_name_plural = 'Inhaltsblöcke'
|
verbose_name_plural = 'Inhaltsblöcke'
|
||||||
|
|
||||||
NORMAL = 'normal'
|
NORMAL = 'normal'
|
||||||
BASE_COMMUNICATION = 'base_communication'
|
|
||||||
TASK = 'task'
|
TASK = 'task'
|
||||||
BASE_SOCIETY = 'base_society'
|
INSTRUMENT = 'instrument'
|
||||||
BASE_INTERDISCIPLINARY = 'base_interdisciplinary'
|
|
||||||
|
|
||||||
TYPE_CHOICES = (
|
TYPE_CHOICES = (
|
||||||
(NORMAL, 'Normal'),
|
(NORMAL, 'Normal'),
|
||||||
(BASE_COMMUNICATION, 'Instrument Sprache & Kommunikation'),
|
|
||||||
(TASK, 'Auftrag'),
|
(TASK, 'Auftrag'),
|
||||||
(BASE_SOCIETY, 'Instrument Gesellschaft'),
|
(INSTRUMENT, 'Instrument'),
|
||||||
(BASE_INTERDISCIPLINARY, 'Überfachliches Instrument'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# blocks without owner are visible by default, need to be hidden for each class
|
# blocks without owner are visible by default, need to be hidden for each class
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import graphene
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
|
from basicknowledge.models import BasicKnowledge
|
||||||
|
from basicknowledge.queries import InstrumentCategoryNode
|
||||||
from books.models import ContentBlock
|
from books.models import ContentBlock
|
||||||
from books.schema.interfaces.contentblock import ContentBlockInterface
|
from books.schema.interfaces.contentblock import ContentBlockInterface
|
||||||
from books.utils import are_solutions_enabled_for
|
from books.utils import are_solutions_enabled_for
|
||||||
|
|
@ -40,6 +42,7 @@ class ContentBlockNode(DjangoObjectType, HiddenAndVisibleForMixin):
|
||||||
mine = graphene.Boolean()
|
mine = graphene.Boolean()
|
||||||
bookmarks = graphene.List(ContentBlockBookmarkNode)
|
bookmarks = graphene.List(ContentBlockBookmarkNode)
|
||||||
original_creator = graphene.Field('users.schema.PublicUserNode')
|
original_creator = graphene.Field('users.schema.PublicUserNode')
|
||||||
|
instrument_category = graphene.Field(InstrumentCategoryNode)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContentBlock
|
model = ContentBlock
|
||||||
|
|
@ -80,6 +83,17 @@ class ContentBlockNode(DjangoObjectType, HiddenAndVisibleForMixin):
|
||||||
content_block=self
|
content_block=self
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_instrument_category(root: ContentBlock, info, **kwargs):
|
||||||
|
if root.type == ContentBlock.INSTRUMENT:
|
||||||
|
for content in root.contents.raw_data:
|
||||||
|
if content['type'] == 'instrument' or content['type'] == 'basic_knowledge':
|
||||||
|
_id = content['value']['basic_knowledge']
|
||||||
|
instrument = BasicKnowledge.objects.get(id=_id)
|
||||||
|
category = instrument.new_type.category
|
||||||
|
return category
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def process_module_room_slug_block(content):
|
def process_module_room_slug_block(content):
|
||||||
if content['type'] == 'module_room_slug':
|
if content['type'] == 'module_room_slug':
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ module_1_chapter_2 = {
|
||||||
'description': 'Haben Sie sich beim Shoppen schon mal überlegt, aus welchem Beweggrund Sie ein bestimmtes Produkt eigentlich unbedingt haben wollten? Wir gehen im Folgenden anhand Ihres letzten Kleiderkaufs dieser Frage nach.',
|
'description': 'Haben Sie sich beim Shoppen schon mal überlegt, aus welchem Beweggrund Sie ein bestimmtes Produkt eigentlich unbedingt haben wollten? Wir gehen im Folgenden anhand Ihres letzten Kleiderkaufs dieser Frage nach.',
|
||||||
'content_blocks': [
|
'content_blocks': [
|
||||||
{
|
{
|
||||||
'type': 'base_society',
|
'type': 'instrument',
|
||||||
'title': 'Das Berufsbildungssystem',
|
'title': 'Das Berufsbildungssystem',
|
||||||
'contents': [
|
'contents': [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,7 @@ type ContentBlockNode implements Node & ContentBlockInterface {
|
||||||
mine: Boolean
|
mine: Boolean
|
||||||
bookmarks: [ContentBlockBookmarkNode]
|
bookmarks: [ContentBlockBookmarkNode]
|
||||||
originalCreator: PublicUserNode
|
originalCreator: PublicUserNode
|
||||||
|
instrumentCategory: InstrumentCategoryNode
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentBlockNodeConnection {
|
type ContentBlockNodeConnection {
|
||||||
|
|
@ -511,7 +512,7 @@ type InstrumentBookmarkNode implements Node {
|
||||||
instrument: InstrumentNode!
|
instrument: InstrumentNode!
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstrumentCategoryNode {
|
type InstrumentCategoryNode implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
name: String!
|
name: String!
|
||||||
background: String!
|
background: String!
|
||||||
|
|
@ -539,7 +540,7 @@ type InstrumentNodeEdge {
|
||||||
cursor: String!
|
cursor: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstrumentTypeNode {
|
type InstrumentTypeNode implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
name: String!
|
name: String!
|
||||||
category: InstrumentCategoryNode
|
category: InstrumentCategoryNode
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue