Loading editor/src/importExport.ts +190 −1 Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ import { parse, stringify } from 'yaml' import packageJson from '../package.json' import { db } from './indexeddb/db' import { getConfig, getUniqueChannel } from './indexeddb/operations' import type { Milestone } from './indexeddb/types' import { InjectType, type ConfigTable, Loading @@ -13,7 +14,7 @@ import { type Question, type Questionnaire, } from './indexeddb/types' import { applyContentPathsDeep, defaultControl } from './utils' import { applyContentPathsDeep, defaultControl, getNewControl } from './utils' import { DRIVE_FOLDER_NAME, FILES_FOLDER_NAME } from './zip/utils' const TABLE_TO_FILE: Record<string, string> = { Loading Loading @@ -421,3 +422,191 @@ export const loadDbData = async (zip: JSZip) => { await importFiles(zip) await importDrive(zip) } export const loadInjectPattern = async (zip: JSZip) => { const contentMap = await loadContentFiles(zip, 'content') const llmMap = await loadContentFiles(zip, 'llm') let milestones: Milestone[] = [] let injects: Inject[] = [] let questionnaires: Questionnaire[] = [] for (const [fileName, tableName] of Object.entries(FILE_TO_TABLE)) { const records = await loadYamlData(zip, fileName) if (!Array.isArray(records) || records.length === 0) { continue } applyContentPathsDeep(records, contentMap, llmMap, false) const table = db.table(tableName) if (!table || typeof table !== 'object') { throw new Error('Wrong TABLE_TO_FILE name.') } if (tableName === 'inject') { injects = records.map(normalizeInject) continue } if (tableName === 'questionnaires') { questionnaires = records.map(normalizeQuestionnaire) continue } if (tableName === 'milestones') { milestones = records as Milestone[] continue } const tableKeys = new Set(await table.toCollection().primaryKeys()) const filtered = records.filter(record => !tableKeys.has(record.name)) await table.bulkPut(filtered) } const milestoneNameSet = new Set( (await db.milestones.toArray()).map(ms => ms.name) ) const changedMilestonesMap = new Map<string, string>() // OLD NAME - NEW NAME const changedMilestones = milestones.map(milestone => { if (!milestoneNameSet.has(milestone.name)) { milestoneNameSet.add(milestone.name) return milestone } let i = 0 while (milestoneNameSet.has(`${milestone.name}_${i}`)) { i += 1 } const oldName = milestone.name const newName = `${milestone.name}_${i}` changedMilestonesMap.set(oldName, newName) milestoneNameSet.add(newName) return { ...milestone, name: `${milestone.name}_${i}` } }) const injectNamesSet = new Set( (await db.inject.toArray()).map(inject => inject.name) ) const changedInjects = injects.map(inject => { let name = inject.name let index = 0 while (injectNamesSet.has(name)) { name = `${inject.name}_${index}` index += 1 } injectNamesSet.add(name) if (inject.type === InjectType.INFORMATION) { const control = inject.alternatives[0].control ? getNewControl(inject.alternatives[0].control, changedMilestonesMap) : undefined const confirmation = inject.alternatives[0].confirmation ? { ...inject.alternatives[0].confirmation, control: getNewControl( inject.alternatives[0].confirmation.control, changedMilestonesMap ), } : undefined return { ...inject, name, alternatives: [ { ...inject.alternatives[0], control, confirmation, }, ], } } else { const control = inject.alternatives[0].control ? getNewControl(inject.alternatives[0].control, changedMilestonesMap) : undefined return { ...inject, name, alternatives: [{ ...inject.alternatives[0], control }], } } }) const questionnaireNamesSet = new Set( (await db.questionnaires.toArray()).map(questionnaire => questionnaire.name) ) const changedQuestionnaires = questionnaires.map(questionnaire => { let name = questionnaire.name let index = 0 while (questionnaireNamesSet.has(name)) { name = `${questionnaire.name}_${index}` index += 1 } questionnaireNamesSet.add(name) const control = questionnaire.control ? getNewControl(questionnaire.control, changedMilestonesMap) : undefined const repeatable = questionnaire.repeatable ? { ...questionnaire.repeatable, on_fail: questionnaire.repeatable.on_fail ? getNewControl( questionnaire.repeatable.on_fail, changedMilestonesMap ) : undefined, } : undefined const questions = questionnaire.questions.map(question => { switch (question.type) { case 'multiple-choice': case 'radio': { const controls = Array.isArray(question.controls) ? question.controls.map(control => getNewControl(control, changedMilestonesMap) ) : question.controls return { ...question, controls } } case 'auto-free-form': { const correct = question.correct ? getNewControl(question.correct, changedMilestonesMap) : undefined const incorrect = question.incorrect ? getNewControl(question.incorrect, changedMilestonesMap) : undefined return { ...question, correct, incorrect } } case 'free-form': { const related_milestones = question.related_milestones?.map( oldName => { const newName = changedMilestonesMap.get(oldName) return newName ? newName : oldName } ) return { ...question, related_milestones } } } }) return { ...questionnaire, control, questions, repeatable } }) await db.milestones.bulkPut(changedMilestones) await db.inject.bulkPut(changedInjects) await db.questionnaires.bulkPut(changedQuestionnaires) await importFiles(zip) await importDrive(zip) } editor/src/indexeddb/types.ts +1 −1 Original line number Diff line number Diff line Loading @@ -318,7 +318,7 @@ export type MultipleChoiceQuestion = { labels: string[] correct: number[] exact_match?: boolean controls: Control[] | Map<number, Control> controls?: Control[] | Map<number, Control> } export type ContentFile = { Loading editor/src/utils.ts +23 −0 Original line number Diff line number Diff line Loading @@ -499,3 +499,26 @@ export const uploadDefinitionZip = async () => { credentials: 'include', }).then(result => result.json()) } export const getNewControl = ( control: Control, changedMilestonesMap: Map<string, string> ) => { control.activate_milestone = control.activate_milestone?.map(oldName => { const newName = changedMilestonesMap.get(oldName) return newName ? newName : oldName }) control.deactivate_milestone = control.deactivate_milestone?.map(oldName => { const newName = changedMilestonesMap.get(oldName) return newName ? newName : oldName }) control.milestone_condition = control.milestone_condition ?.split(' ') .map(activate_ms => { const newName = changedMilestonesMap.get(activate_ms) return newName ? newName : activate_ms }) .join(' ') return control } Loading
editor/src/importExport.ts +190 −1 Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ import { parse, stringify } from 'yaml' import packageJson from '../package.json' import { db } from './indexeddb/db' import { getConfig, getUniqueChannel } from './indexeddb/operations' import type { Milestone } from './indexeddb/types' import { InjectType, type ConfigTable, Loading @@ -13,7 +14,7 @@ import { type Question, type Questionnaire, } from './indexeddb/types' import { applyContentPathsDeep, defaultControl } from './utils' import { applyContentPathsDeep, defaultControl, getNewControl } from './utils' import { DRIVE_FOLDER_NAME, FILES_FOLDER_NAME } from './zip/utils' const TABLE_TO_FILE: Record<string, string> = { Loading Loading @@ -421,3 +422,191 @@ export const loadDbData = async (zip: JSZip) => { await importFiles(zip) await importDrive(zip) } export const loadInjectPattern = async (zip: JSZip) => { const contentMap = await loadContentFiles(zip, 'content') const llmMap = await loadContentFiles(zip, 'llm') let milestones: Milestone[] = [] let injects: Inject[] = [] let questionnaires: Questionnaire[] = [] for (const [fileName, tableName] of Object.entries(FILE_TO_TABLE)) { const records = await loadYamlData(zip, fileName) if (!Array.isArray(records) || records.length === 0) { continue } applyContentPathsDeep(records, contentMap, llmMap, false) const table = db.table(tableName) if (!table || typeof table !== 'object') { throw new Error('Wrong TABLE_TO_FILE name.') } if (tableName === 'inject') { injects = records.map(normalizeInject) continue } if (tableName === 'questionnaires') { questionnaires = records.map(normalizeQuestionnaire) continue } if (tableName === 'milestones') { milestones = records as Milestone[] continue } const tableKeys = new Set(await table.toCollection().primaryKeys()) const filtered = records.filter(record => !tableKeys.has(record.name)) await table.bulkPut(filtered) } const milestoneNameSet = new Set( (await db.milestones.toArray()).map(ms => ms.name) ) const changedMilestonesMap = new Map<string, string>() // OLD NAME - NEW NAME const changedMilestones = milestones.map(milestone => { if (!milestoneNameSet.has(milestone.name)) { milestoneNameSet.add(milestone.name) return milestone } let i = 0 while (milestoneNameSet.has(`${milestone.name}_${i}`)) { i += 1 } const oldName = milestone.name const newName = `${milestone.name}_${i}` changedMilestonesMap.set(oldName, newName) milestoneNameSet.add(newName) return { ...milestone, name: `${milestone.name}_${i}` } }) const injectNamesSet = new Set( (await db.inject.toArray()).map(inject => inject.name) ) const changedInjects = injects.map(inject => { let name = inject.name let index = 0 while (injectNamesSet.has(name)) { name = `${inject.name}_${index}` index += 1 } injectNamesSet.add(name) if (inject.type === InjectType.INFORMATION) { const control = inject.alternatives[0].control ? getNewControl(inject.alternatives[0].control, changedMilestonesMap) : undefined const confirmation = inject.alternatives[0].confirmation ? { ...inject.alternatives[0].confirmation, control: getNewControl( inject.alternatives[0].confirmation.control, changedMilestonesMap ), } : undefined return { ...inject, name, alternatives: [ { ...inject.alternatives[0], control, confirmation, }, ], } } else { const control = inject.alternatives[0].control ? getNewControl(inject.alternatives[0].control, changedMilestonesMap) : undefined return { ...inject, name, alternatives: [{ ...inject.alternatives[0], control }], } } }) const questionnaireNamesSet = new Set( (await db.questionnaires.toArray()).map(questionnaire => questionnaire.name) ) const changedQuestionnaires = questionnaires.map(questionnaire => { let name = questionnaire.name let index = 0 while (questionnaireNamesSet.has(name)) { name = `${questionnaire.name}_${index}` index += 1 } questionnaireNamesSet.add(name) const control = questionnaire.control ? getNewControl(questionnaire.control, changedMilestonesMap) : undefined const repeatable = questionnaire.repeatable ? { ...questionnaire.repeatable, on_fail: questionnaire.repeatable.on_fail ? getNewControl( questionnaire.repeatable.on_fail, changedMilestonesMap ) : undefined, } : undefined const questions = questionnaire.questions.map(question => { switch (question.type) { case 'multiple-choice': case 'radio': { const controls = Array.isArray(question.controls) ? question.controls.map(control => getNewControl(control, changedMilestonesMap) ) : question.controls return { ...question, controls } } case 'auto-free-form': { const correct = question.correct ? getNewControl(question.correct, changedMilestonesMap) : undefined const incorrect = question.incorrect ? getNewControl(question.incorrect, changedMilestonesMap) : undefined return { ...question, correct, incorrect } } case 'free-form': { const related_milestones = question.related_milestones?.map( oldName => { const newName = changedMilestonesMap.get(oldName) return newName ? newName : oldName } ) return { ...question, related_milestones } } } }) return { ...questionnaire, control, questions, repeatable } }) await db.milestones.bulkPut(changedMilestones) await db.inject.bulkPut(changedInjects) await db.questionnaires.bulkPut(changedQuestionnaires) await importFiles(zip) await importDrive(zip) }
editor/src/indexeddb/types.ts +1 −1 Original line number Diff line number Diff line Loading @@ -318,7 +318,7 @@ export type MultipleChoiceQuestion = { labels: string[] correct: number[] exact_match?: boolean controls: Control[] | Map<number, Control> controls?: Control[] | Map<number, Control> } export type ContentFile = { Loading
editor/src/utils.ts +23 −0 Original line number Diff line number Diff line Loading @@ -499,3 +499,26 @@ export const uploadDefinitionZip = async () => { credentials: 'include', }).then(result => result.json()) } export const getNewControl = ( control: Control, changedMilestonesMap: Map<string, string> ) => { control.activate_milestone = control.activate_milestone?.map(oldName => { const newName = changedMilestonesMap.get(oldName) return newName ? newName : oldName }) control.deactivate_milestone = control.deactivate_milestone?.map(oldName => { const newName = changedMilestonesMap.get(oldName) return newName ? newName : oldName }) control.milestone_condition = control.milestone_condition ?.split(' ') .map(activate_ms => { const newName = changedMilestonesMap.get(activate_ms) return newName ? newName : activate_ms }) .join(' ') return control }