feat: Initial Scaffold for QTI editor components and add a development page#5963
feat: Initial Scaffold for QTI editor components and add a development page#5963Abhishek-Punhani wants to merge 1 commit into
Conversation
…t page Signed-off-by: Abhishek-Punhani <punhani.manavabhi@gmail.com>
|
👋 Hi @Abhishek-Punhani, thanks for contributing! For the review process to begin, please verify that the following is satisfied:
Also check that issue requirements are satisfied & you ran Pull requests that don't follow the guidelines will be closed. Reviewer assignment can take up to 2 weeks. |
|
📢✨ Before we assign a reviewer, we'll turn on |
rtibblesbot
left a comment
There was a problem hiding this comment.
Clean, well-scoped scaffold — the Composition API architecture, immutable update pattern, and i18n discipline all land well.
CI passing. Screenshot shows all three hardcoded cards rendering with working up/down/delete/add controls. All acceptance criteria met.
- suggestion (3):
canEditprop is inert with no keyboard path to open cards;QTIItemEditormissingemitsdeclaration (inconsistent withQTIEditor);transition-groupanimation CSS missing - nitpick (1):
setTimeoutvsnextTickinaddItem
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
Reviewed the pull request diff checking for:
- Correctness: bugs, edge cases, undocumented behavior, resource leaks, hardcoded values
- Design: unnecessary complexity, naming, readability, comment accuracy, redundant state
- Architecture: duplicated concerns, minimal interfaces, composition over inheritance
- Testing: behavior-based assertions, mocks only at hard boundaries, accurate coverage
- Completeness: missing dependencies, unupdated usages, i18n, accessibility, security
- Principles: DRY (same reason to change), SRP, Rule of Three (no premature abstraction)
- Checked CI status and linked issue acceptance criteria
- For UI changes: inspected screenshots for layout, visual completeness, and consistency
| :displayMenu="true" | ||
| :canMoveUp="canMoveUp" | ||
| :canMoveDown="canMoveDown" | ||
| :canEdit="!isOpen" |
There was a problem hiding this comment.
suggestion: :canEdit="!isOpen" is silently inert. AssessmentItemToolbar only uses the canEdit prop to gate the EDIT_ITEM action, but EDIT_ITEM is not included in either iconActionsConfig or menuActionsConfig here. The prop has no runtime effect.
The larger consequence: there is no keyboard-accessible way to open a closed card. The only open path is @click.native="onCardClick" on KPageContainer (a non-focusable <div>), which doesn't respond to Enter/Space. Consider adding [AssessmentItemToolbarActions.EDIT_ITEM, { collapse: true }] to iconActionsConfig when the card is closed, and removing the now-dead :canEdit binding.
| }; | ||
|
|
||
| export default defineComponent({ | ||
| name: 'QTIItemEditor', |
There was a problem hiding this comment.
suggestion: This component emits open, close, and action but has no emits option declared. QTIEditor (the sibling) correctly has emits: ['update']. For consistency and to give tooling the event contract, add:
emits: ['open', 'close', 'action'],| </div> | ||
| </KPageContainer> | ||
|
|
||
| <transition-group |
There was a problem hiding this comment.
suggestion: transition-group name="list-complete" requires corresponding CSS classes (.list-complete-enter, .list-complete-leave-to, .list-complete-move, etc.) to produce any animation. None are defined in the scoped styles, so the transition silently does nothing. Either add the CSS or remove the name attribute until the animation is ready to implement.
| list.splice(pos, 0, newItem); | ||
| emit('update', list); | ||
| // Open the newly created card on the next tick | ||
| setTimeout(() => { |
There was a problem hiding this comment.
nitpick: setTimeout(() => { activeId.value = newItem.id; }, 0) works, but nextTick (imported from 'vue') is the idiomatic Vue way to defer until after the DOM update cycle and is more explicit about intent.
| activeId.value = null; | ||
| } | ||
|
|
||
| const cloneList = () => [...props.assessments]; |
There was a problem hiding this comment.
praise: The cloneList() + emit pattern throughout addItem, deleteItem, moveItemUp, moveItemDown is exactly right for the Vuex-independent prop-down/event-up contract — the parent always gets a fresh array and the component never mutates the prop directly.
| const NAMESPACE = 'QTIEditorStrings'; | ||
|
|
||
| const MESSAGES = { | ||
| noQuestionsPlaceholder: { |
There was a problem hiding this comment.
praise: Every string has both message and context, which is what translators need to produce accurate, in-context translations. Good discipline at the scaffolding stage.
AlexVelezLl
left a comment
There was a problem hiding this comment.
Thanks @Abhishek-Punhani! I've found some areas for improvement and a couple of things that weren't specified in the original issue 😅.
| TRASH: 'TRASH', | ||
| ADD_PREVIOUS_STEPS: 'ADD_PREVIOUS_STEPS', | ||
| ADD_NEXT_STEPS: 'ADD_NEXT_STEPS', | ||
| QTI_DEMO: 'QTI_DEMO', |
There was a problem hiding this comment.
Oh, let's just use a hardcoded string 😅, so that we don't forget to remove this constant later.
| > | ||
| <div | ||
| class="question-card-header" | ||
| :style="{ borderBottom: isOpen ? `1px solid ${$themePalette.grey.v_200}` : 'none' }" |
There was a problem hiding this comment.
Could we use $themeTokens.fineLine instead?
| <AssessmentItemToolbar | ||
| :iconActionsConfig="iconActionsConfig" | ||
| :menuActionsConfig="menuActionsConfig" | ||
| :displayMenu="true" | ||
| :canMoveUp="canMoveUp" | ||
| :canMoveDown="canMoveDown" | ||
| :canEdit="!isOpen" | ||
| :collapse="windowIsSmall" | ||
| :itemLabel="toolbarItemLabel" | ||
| analyticsLabel="QTI Question" | ||
| data-test="toolbar" | ||
| @click="action => $emit('action', action)" | ||
| /> |
There was a problem hiding this comment.
Ah, apologies, I didn't spec this. Could we create a new CollapsibleToolbar component in the components folder? Let's make this component more generic and reusable, and instead of having so many hardcoded "canMoveUp", "canMoveDown", etc. props, let's make this component receive a single array with the structure:
[{
"icon": "", (string or null)
"label": "", (required)
"handler": () => {},
"collapsed": true/false
}]So... collapsed will determine if this should go into the dropdown menu; the dropdown menu should only be visible if there is at least one item collapsed. For items that are always collapsed, "collapsed": true; if it depends, we can write the condition here, e.g., "collapsed": windowIsSmall.value. And we can filter out any actions that are not needed (e.g., the first item should not have move up).
|
|
||
| import { computed, defineComponent } from 'vue'; | ||
| import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow'; | ||
| import { useQTIStr } from '../../qtiEditorStrings'; |
There was a problem hiding this comment.
Could we use the same structure as the communityChannelsStrings file? Where the whole translator is exposed as a named export, and we use destructuring to get the values in the setup. e.g. here.
| import AssessmentItemToolbar from 'frontend/channelEdit/components/AssessmentItemToolbar'; | ||
| import { AssessmentItemToolbarActions } from 'frontend/channelEdit/constants'; |
There was a problem hiding this comment.
In general, we should always try to avoid importing from other modules outside the QTIEditor as much as possible.
| const { windowIsSmall } = useKResponsiveWindow(); | ||
|
|
||
| const containerStyle = computed(() => | ||
| windowIsSmall.value ? {} : { maxWidth: '85%', margin: '0 auto' }, |
There was a problem hiding this comment.
Some visual specs have changed (apologies 😅), now maxWidth should be a plain 1200px and the padding/margin should be 16px on small screens and 32px on other screens.
| activeId.value = null; | ||
| } | ||
|
|
||
| const cloneList = () => [...props.assessments]; |
There was a problem hiding this comment.
cloneList isn't fully descriptive, and we are not deep cloning the list; just wondering if it'd be clearer just to write [...props.assessments] where we need this.
| // Open the newly created card on the next tick | ||
| setTimeout(() => { | ||
| activeId.value = newItem.id; | ||
| }, 0); |
There was a problem hiding this comment.
We don't really need to wait until the next tick, right? we can just set the activeId and whenever the list is updated, the activeId will be set.
| if (activeId.value === item.id) closeItem(); | ||
| emit( | ||
| 'update', | ||
| cloneList().filter(i => i.id !== item.id), |
There was a problem hiding this comment.
Here we dont really need to clone the list because filter already returns a new list.
| function onItemAction(action, item, idx) { | ||
| switch (action) { | ||
| case AssessmentItemToolbarActions.EDIT_ITEM: | ||
| openItem(item.id); | ||
| break; | ||
| case AssessmentItemToolbarActions.DELETE_ITEM: | ||
| deleteItem(item); | ||
| break; | ||
| case AssessmentItemToolbarActions.ADD_ITEM_ABOVE: | ||
| addItem({ atIndex: idx }); | ||
| break; | ||
| case AssessmentItemToolbarActions.ADD_ITEM_BELOW: | ||
| addItem({ atIndex: idx + 1 }); | ||
| break; | ||
| case AssessmentItemToolbarActions.MOVE_ITEM_UP: | ||
| moveItemUp(idx); | ||
| break; | ||
| case AssessmentItemToolbarActions.MOVE_ITEM_DOWN: | ||
| moveItemDown(idx); | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
I think it may be easier for us to just use a toolbarActions slot on the QTIItemEditor and manage all these actions directly on the QTIEditor component, which is the component that actually handles the actions.
Summary
This PR introduces the initial frontend scaffolding for a brand-new QTI 3.0 authoring editor.
References
Closes #5961
Reviewer guidance
/#/qti-demo.AI usage
Used Antigravity for final review and nitpicks.