diff --git a/test/stubs.mjs b/test/stubs.mjs index 1e5c483..6ba42f0 100644 --- a/test/stubs.mjs +++ b/test/stubs.mjs @@ -80,6 +80,12 @@ async function getTestData(testDataId) { field: { name: 'Meeting' } }); } + if (session.trymeout) { + fields.push({ + text: session.trymeout, + field: { name: 'Try me out' } + }); + } return { id: `id_${uid++}`, @@ -125,6 +131,9 @@ async function getTestData(testDataId) { if (custom.allowMultipleMeetings) { testData.allowMultipleMeetings = custom.allowMultipleMeetings; } + if (custom.allowTryMeOut) { + testData.allowTryMeOut = custom.allowTryMeOut; + } testDataCache[testDataId] = testData; return JSON.parse(JSON.stringify(testData)); @@ -156,6 +165,9 @@ export async function sendGraphQLRequest(query, acceptHeader = '') { if ((name === 'Meeting') && !testData.allowMultipleMeetings) { field = null; } + else if ((name === 'Try me out') && !testData.allowTryMeOut) { + field = null; + } else { field = { id: `id_field_${name}`, diff --git a/tools/cli.mjs b/tools/cli.mjs index f01a05a..79bf67b 100644 --- a/tools/cli.mjs +++ b/tools/cli.mjs @@ -17,6 +17,7 @@ import schedule from './commands/schedule.mjs'; import synchronizeCalendar from './commands/sync-calendar.mjs'; import validate from './commands/validate.mjs'; import viewEvent from './commands/view-event.mjs'; +import tryChanges from './commands/try-changes.mjs'; /** @@ -188,6 +189,30 @@ Usage notes for the options: `); +/****************************************************************************** + * The "try-changes" command + *****************************************************************************/ +program + .command('try-changes') + .summary('Try schedule changes in "Try me out" field.') + .description('Update the schedule with the meeting changes proposed in the "Try me out" field and report the adjusted grid and validation issues.') + .option('-a, --apply', 'apply the adjusted schedule, updating events information on GitHub') + .action(getCommandRunner(tryChanges)) + .addHelpText('after', ` +Output: + The command returns the generated schedule grid as HTML content (same structure as the one returned by the \`view\` command). You may want to redirect the output to a file. For example: + $ npx tpac-breakouts try-changes > grid.html + + The command also emits warnings to the console to report on progress. + +Usage notes for the options: +-a, --apply + When the option is not set, the command merely reports the adjusted schedule. + + When the option is set, the command applies the adjusted schedule, meaning it updates the scheduling information in the GitHub project associated with the event repository. It resets the "Try me out" field accordingly. +`); + + /****************************************************************************** * The "sync-calendar" command *****************************************************************************/ diff --git a/tools/commands/try-changes.mjs b/tools/commands/try-changes.mjs new file mode 100644 index 0000000..9b9cbea --- /dev/null +++ b/tools/commands/try-changes.mjs @@ -0,0 +1,60 @@ +import { validateGrid } from '../lib/validate.mjs'; +import { saveSessionMeetings } from '../lib/project.mjs'; +import { convertProjectToHTML } from '../lib/project2html.mjs'; + +export default async function (project, options) { + if (!project.allowTryMeOut) { + throw new Error(`No "Try me out" custom field in project`); + } + + if (!project.sessions.find(session => session.trymeout)) { + throw new Error(`No schedule adjustments to try!`); + } + + console.warn(); + console.warn(`Adjust schedule with "Try me out" field...`); + for (const session of project.sessions) { + if (session.trymeout) { + // TODO: also support "try me out" for breakout sessions, + // updating the room, day and slot fields instead of meeting which + // does not exist for breakout sessions. + session.meeting = session.trymeout; + session.trymeout = null; + session.updated = true; + console.warn(`- session ${session.number}: from "${session.meeting}" to "${session.trymeout}"`); + } + } + console.warn(`Adjust schedule with "Try me out" field... done`); + + console.warn(); + console.warn(`Validate created grid...`); + const { errors: newErrors } = await validateGrid(project, { what: 'scheduling' }) + if (newErrors.length) { + for (const error of newErrors) { + console.warn(`- [${error.severity}: ${error.type}] #${error.session}: ${error.messages.join(', ')}`); + } + } + else { + console.warn(`- looks good!`); + } + console.warn(`Validate created grid... done`); + + console.warn(); + console.warn(`Report project as HTML...`); + const html = await convertProjectToHTML(project); + console.warn(); + console.log(html); + console.warn(`Report project as HTML... done`); + + if (options.apply) { + console.warn(); + console.warn(`Apply created grid...`); + const sessionsToUpdate = project.sessions.filter(s => s.updated); + for (const session of sessionsToUpdate) { + console.warn(`- updating #${session.number}...`); + await saveSessionMeetings(session, project); + console.warn(`- updating #${session.number}... done`); + } + console.warn(`Apply created grid... done`); + } +} diff --git a/tools/lib/project.mjs b/tools/lib/project.mjs index 7ee352c..9173764 100644 --- a/tools/lib/project.mjs +++ b/tools/lib/project.mjs @@ -621,6 +621,21 @@ export async function fetchProject(login, id) { }`); const meeting = meetingResponse.data[type].projectV2.field; + // Project may also have a "Try me out" custom field to adjust the schedule + const tryMeetingResponse = await sendGraphQLRequest(`query { + ${type}(login: "${login}"){ + projectV2(number: ${id}) { + field(name: "Try me out") { + ... on ProjectV2FieldCommon { + id + name + } + } + } + } + }`); + const tryMeeting = tryMeetingResponse.data[type].projectV2.field; + // Another request to retrieve the list of sessions associated with the project. const sessionsResponse = await sendGraphQLRequest(`query { ${type}(login: "${login}") { @@ -802,6 +817,11 @@ export async function fetchProject(login, id) { meetingsFieldId: meeting?.id, allowMultipleMeetings: !!meeting?.id, + // ID of the "Try me out" custom field, if it exists + // (it signals the ability to try schedule adjustments from GitHub) + trymeoutsFieldId: tryMeeting?.id, + allowTryMeOut: !!tryMeeting?.id, + // Sections defined in the issue template sessionSections, @@ -832,6 +852,8 @@ export async function fetchProject(login, id) { .find(value => value.field?.name === 'Slot')?.name, meeting: session.fieldValues.nodes .find(value => value.field?.name === 'Meeting')?.text, + trymeout: session.fieldValues.nodes + .find(value => value.field?.name === 'Try me out')?.text, validation: { check: session.fieldValues.nodes.find(value => value.field?.name === 'Check')?.text, warning: session.fieldValues.nodes.find(value => value.field?.name === 'Warning')?.text, @@ -870,12 +892,12 @@ function parseProjectDescription(desc) { * Record the meetings assignments for the provided session */ export async function saveSessionMeetings(session, project) { - for (const field of ['room', 'day', 'slot', 'meeting']) { + for (const field of ['room', 'day', 'slot', 'meeting', 'trymeout']) { // Project may not allow multiple meetings if (!project[field + 'sFieldId']) { continue; } - const prop = (field === 'meeting') ? 'text': 'singleSelectOptionId'; + const prop = (field === 'meeting' || field === 'trymeout') ? 'text': 'singleSelectOptionId'; let value = null; if (prop === 'text') { // Text field @@ -998,6 +1020,9 @@ export function convertProjectToJSON(project) { if (project.allowMultipleMeetings) { data.allowMultipleMeetings = true; } + if (project.allowTryMeOut) { + data.allowTryMeOut = true; + } for (const list of ['days', 'rooms', 'slots', 'labels']) { data[list] = toNameList(project[list]); } diff --git a/tools/lib/validate.mjs b/tools/lib/validate.mjs index 0f53b7e..f930c40 100644 --- a/tools/lib/validate.mjs +++ b/tools/lib/validate.mjs @@ -13,7 +13,8 @@ const schedulingErrors = [ 'warning: capacity', 'warning: conflict', 'warning: duration', - 'warning: track' + 'warning: track', + 'warning: times' ]; /**