From 973c6c7d6b8cb5ccdba6d5c2fa79a2eda9c8704e Mon Sep 17 00:00:00 2001 From: Nicolas Pavie Date: Tue, 9 Apr 2024 15:56:26 +0200 Subject: [PATCH 01/10] feat(api): new stylesheet-parameters endpoint the endpoint takes a job with inputs and braille stylesheet parameters set (stylesheet, page-width and page-height options) and returns a set of additionnal parameters, converted in script options to use for completing a job request. --- src/shared/data/apis/pipeline.ts | 20 ++++++++++ .../jobToStylesheetParametersXml.ts | 38 +++++++++++++++++++ .../parametersXmlToJson.ts | 29 ++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 src/shared/parser/pipelineXmlConverter/jobToStylesheetParametersXml.ts create mode 100644 src/shared/parser/pipelineXmlConverter/parametersXmlToJson.ts diff --git a/src/shared/data/apis/pipeline.ts b/src/shared/data/apis/pipeline.ts index 49634ea..9b99ef3 100644 --- a/src/shared/data/apis/pipeline.ts +++ b/src/shared/data/apis/pipeline.ts @@ -26,6 +26,8 @@ import { jobResponseXmlToJson } from 'shared/parser/pipelineXmlConverter/jobResp import { propertiesXmlToJson } from 'shared/parser/pipelineXmlConverter/propertiesXmlToJson' import { propertyToXml } from 'shared/parser/pipelineXmlConverter/propertyToXml' import { ttsEnginesToJson } from 'shared/parser/pipelineXmlConverter/ttsEnginesToJson' +import { parametersXmlToJson } from 'shared/parser/pipelineXmlConverter/parametersXmlToJson' +import { jobToStylesheetParametersXml } from 'shared/parser/pipelineXmlConverter/jobToStylesheetParametersXml' //import fetch, { Response, RequestInit } from 'node-fetch' //import { info, error } from 'electron-log' @@ -204,4 +206,22 @@ export class PipelineAPI { (text) => ttsEnginesToJson(text) ) } + // New /stylesheet-parameters endpoint : https://github.com/daisy/pipeline-ui/issues/198 + // and https://github.com/daisy/pipeline/issues/750 + /** + * Fetch new script options from a braille targeted job with + * inputs, stylesheet, page-width and page-height parameters set + * @param j the braille job + * @returns the script options to use for the job + */ + fetchStylesheetParameters(j: Job) { + return this.createPipelineFetchFunction( + (ws) => `${baseurl(ws)}/stylesheet-parameters`, + (text) => parametersXmlToJson(text), + { + method: 'POST', + body: jobToStylesheetParametersXml(j), + } + ) + } } diff --git a/src/shared/parser/pipelineXmlConverter/jobToStylesheetParametersXml.ts b/src/shared/parser/pipelineXmlConverter/jobToStylesheetParametersXml.ts new file mode 100644 index 0000000..5410547 --- /dev/null +++ b/src/shared/parser/pipelineXmlConverter/jobToStylesheetParametersXml.ts @@ -0,0 +1,38 @@ +import { Job } from 'shared/types' + +/** + * Build a request xml string that can be sent to pipeline on the + * "stylesheet-parameters" using the job inputs and the following + * identified options : + * - stylesheet + * - page-width + * - page-height + * @param {Job} j the job to be used for building the xml + * @returns {string} an xml string that can be sent to a DP2 engine to get + * new script parameters + */ +function jobToStylesheetParametersXml(j: Job): string { + const stylesheet = j.jobRequest.options.filter( + (option) => option.name === 'stylesheet' + )[0] + const width = j.jobRequest.options.filter( + (option) => option.name === 'page-width' + )[0] + const height = j.jobRequest.options.filter( + (option) => option.name === 'page-height' + )[0] + return ` + + ${ + stylesheet !== undefined && `` + } + ${j.jobRequest.inputs + .filter((input) => input.isFile && !input.name.endsWith('.scss')) + .map((input) => ``) + .join('')} +` +} + +export { jobToStylesheetParametersXml } diff --git a/src/shared/parser/pipelineXmlConverter/parametersXmlToJson.ts b/src/shared/parser/pipelineXmlConverter/parametersXmlToJson.ts new file mode 100644 index 0000000..8493680 --- /dev/null +++ b/src/shared/parser/pipelineXmlConverter/parametersXmlToJson.ts @@ -0,0 +1,29 @@ +import { Job, ScriptOption } from 'shared/types' +import { parseXml } from './parser' + +// Get new script options from a stylesheet parameters options +function parametersXmlToJson(xmlString: string): ScriptOption[] { + let parametersElm = parseXml(xmlString, 'parameters') + const result: ScriptOption[] = [] + for (const propElem of parametersElm.getElementsByTagName('parameter')) { + const name = propElem.getAttribute('name') + const value = propElem.getAttribute('default') + const kind = propElem.getAttribute('type') + const nicename = propElem.getAttribute('nicename') + const description = propElem.getAttribute('description') + result.push({ + desc: propElem.getAttribute('description'), + name: propElem.getAttribute('name'), + sequence: propElem.getAttribute('sequence') == 'true', // should always be false + required: propElem.getAttribute('required') == 'true', // should always be false + nicename: propElem.getAttribute('nicename'), + ordered: propElem.getAttribute('ordered') == 'true', // should always be false + type: propElem.getAttribute('type'), + default: propElem.getAttribute('default'), + kind: 'option', + } as ScriptOption) + } + return result +} + +export { parametersXmlToJson } From 191843067e2388b379154f7802bfaebd5a07ad77 Mon Sep 17 00:00:00 2001 From: Nicolas Pavie Date: Tue, 9 Apr 2024 15:57:00 +0200 Subject: [PATCH 02/10] feat(api): expose and use new endpoint through middleware - new requestStylesheetParameters action on pipeline slice - new optionnal stylesheetParameters property on job type to store additionnal script options - requestStylesheetParameters handled by the middleware to use the stylesheet-parameters endpoint from a given job and store the result in job.stylesheetParameters property --- src/main/data/middlewares/pipeline.ts | 14 ++++++++++++++ src/shared/data/slices/pipeline.ts | 16 +++++++++++++++- src/shared/types/pipeline.ts | 5 +++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/data/middlewares/pipeline.ts b/src/main/data/middlewares/pipeline.ts index f188d77..d46001a 100644 --- a/src/main/data/middlewares/pipeline.ts +++ b/src/main/data/middlewares/pipeline.ts @@ -24,6 +24,7 @@ import { setProperties, setTtsEngineState, setTtsEngineFeatures, + requestStylesheetParameters, } from 'shared/data/slices/pipeline' import { @@ -687,6 +688,19 @@ export function pipelineMiddleware({ getState, dispatch }) { } }, 1000) break + case requestStylesheetParameters.type: + const job = action.payload as Job + pipelineAPI + .fetchStylesheetParameters(job)(webservice) + .then((parameters) => { + dispatch( + updateJob({ + ...job, + stylesheetParameters: parameters, + }) + ) + }) + break default: if (action.type.startsWith('settings/')) { // FIXME : check if local pipeline props have changed and diff --git a/src/shared/data/slices/pipeline.ts b/src/shared/data/slices/pipeline.ts index 5d4437a..7963cce 100644 --- a/src/shared/data/slices/pipeline.ts +++ b/src/shared/data/slices/pipeline.ts @@ -74,7 +74,7 @@ export const pipeline = createSlice({ /** * Stop the pipeline instance * - * (Middleware handled action ) + * (Middleware handled action) * @param state current pipeline state * @param action payload with a boolean : if true, then app is closing */ @@ -239,6 +239,19 @@ export const pipeline = createSlice({ state.selectedJobId = state.jobs[0].internalId } }, + /** + * Request script options from stylesheet parameters endpoint + * + * (Middleware handled action ) + * @param state current pipeline state + * @param action payload with a boolean : if true, then app is closing + */ + requestStylesheetParameters: ( + state: PipelineState, + param: PayloadAction + ) => { + // Handled by the middleware + }, runJob: (state: PipelineState, param: PayloadAction) => { if (param.payload.jobRequest) { // Retrieve latest JobRequest payload @@ -349,6 +362,7 @@ export const { setProperties, setTtsEngineState, setTtsEngineFeatures, + requestStylesheetParameters, } = pipeline.actions export const selectors = { diff --git a/src/shared/types/pipeline.ts b/src/shared/types/pipeline.ts index 6ab0a83..fd2be98 100644 --- a/src/shared/types/pipeline.ts +++ b/src/shared/types/pipeline.ts @@ -199,6 +199,11 @@ export type Job = { invisible?: boolean // jobRequest.script also has script info (returned from ws); // however, storing it separately gives us access to more details + /** + * For job with a stylesheet parameter, supplementary options are retrieved + * from the pipeline stylesheet-parameters end point and stored here. + */ + stylesheetParameters?: ScriptOption[] } // JobData is the JSON representation of Pipeline WS data for a single job export type JobData = { From 298159cadd34004a53255a1df34058115b5a7032 Mon Sep 17 00:00:00 2001 From: Nicolas Pavie Date: Tue, 9 Apr 2024 15:57:00 +0200 Subject: [PATCH 03/10] fix(api): fix parsers for new endpoint --- .../jobToStylesheetParametersXml.ts | 5 +-- .../parametersXmlToJson.ts | 33 ++++++++----------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/shared/parser/pipelineXmlConverter/jobToStylesheetParametersXml.ts b/src/shared/parser/pipelineXmlConverter/jobToStylesheetParametersXml.ts index 5410547..c80a957 100644 --- a/src/shared/parser/pipelineXmlConverter/jobToStylesheetParametersXml.ts +++ b/src/shared/parser/pipelineXmlConverter/jobToStylesheetParametersXml.ts @@ -21,12 +21,13 @@ function jobToStylesheetParametersXml(j: Job): string { const height = j.jobRequest.options.filter( (option) => option.name === 'page-height' )[0] - return ` + return ` + ${ - stylesheet !== undefined && `` + stylesheet && stylesheet.value && `` } ${j.jobRequest.inputs .filter((input) => input.isFile && !input.name.endsWith('.scss')) diff --git a/src/shared/parser/pipelineXmlConverter/parametersXmlToJson.ts b/src/shared/parser/pipelineXmlConverter/parametersXmlToJson.ts index 8493680..6f8aac3 100644 --- a/src/shared/parser/pipelineXmlConverter/parametersXmlToJson.ts +++ b/src/shared/parser/pipelineXmlConverter/parametersXmlToJson.ts @@ -5,25 +5,20 @@ import { parseXml } from './parser' function parametersXmlToJson(xmlString: string): ScriptOption[] { let parametersElm = parseXml(xmlString, 'parameters') const result: ScriptOption[] = [] - for (const propElem of parametersElm.getElementsByTagName('parameter')) { - const name = propElem.getAttribute('name') - const value = propElem.getAttribute('default') - const kind = propElem.getAttribute('type') - const nicename = propElem.getAttribute('nicename') - const description = propElem.getAttribute('description') - result.push({ - desc: propElem.getAttribute('description'), - name: propElem.getAttribute('name'), - sequence: propElem.getAttribute('sequence') == 'true', // should always be false - required: propElem.getAttribute('required') == 'true', // should always be false - nicename: propElem.getAttribute('nicename'), - ordered: propElem.getAttribute('ordered') == 'true', // should always be false - type: propElem.getAttribute('type'), - default: propElem.getAttribute('default'), - kind: 'option', - } as ScriptOption) - } - return result + return Array.from(parametersElm.getElementsByTagName('parameter')).map( + (propElem: any) => + ({ + desc: propElem.getAttribute('description'), + name: propElem.getAttribute('name'), + sequence: propElem.getAttribute('sequence') == 'true', // should always be false + required: propElem.getAttribute('required') == 'true', // should always be false + nicename: propElem.getAttribute('nicename'), + ordered: propElem.getAttribute('ordered') == 'true', // should always be false + type: propElem.getAttribute('type'), + default: propElem.getAttribute('default'), + kind: 'option', + } as ScriptOption) + ) } export { parametersXmlToJson } From 065b7d7f2722b06df90fba5543688f13557d1f45 Mon Sep 17 00:00:00 2001 From: Nicolas Pavie Date: Tue, 9 Apr 2024 15:57:00 +0200 Subject: [PATCH 04/10] feat(ui): multi step job form for script with stylesheets Also: - css and scss filetypes delcaration - type annotations in Fields and ScriptForm - cleaning comments and unused imports in frontend store --- src/renderer/components/Fields/FormField.tsx | 15 ++-- src/renderer/components/ScriptForm/index.tsx | 81 ++++++++++++++++---- src/renderer/store/index.tsx | 62 --------------- src/renderer/utils/utils.ts | 4 +- src/shared/constants/filetypes.ts | 12 ++- 5 files changed, 84 insertions(+), 90 deletions(-) diff --git a/src/renderer/components/Fields/FormField.tsx b/src/renderer/components/Fields/FormField.tsx index 1a4932c..4b11cf2 100644 --- a/src/renderer/components/Fields/FormField.tsx +++ b/src/renderer/components/Fields/FormField.tsx @@ -23,23 +23,22 @@ export function FormField({ }: { item: ScriptItemBase idprefix: string - onChange: (string, ScriptItemBase) => void // function to set the value in a parent-level collection. + onChange: (value: any, item: ScriptItemBase) => void // function to set the value in a parent-level collection. initialValue: any // the initial value for the field }) { const [value, setValue] = useState(initialValue) const [checked, setChecked] = useState(true) let controlId = `${idprefix}-${item.name}` - let onChangeValue = (newValue, scriptItem) => { + let onChangeValue = (newValue: any, scriptItem: ScriptItemBase) => { setValue(newValue) onChange(newValue, scriptItem) } - let dialogOpts = - item.type == 'anyFileURI' - ? ['openFile'] - : item.type == 'anyDirURI' - ? ['openDirectory'] - : ['openFile', 'openDirectory'] + let dialogOpts = ['anyFileURI', 'anyURI'].includes(item.type) + ? ['openFile'] + : item.type == 'anyDirURI' + ? ['openDirectory'] + : ['openFile', 'openDirectory'] const { settings } = useWindowStore() diff --git a/src/renderer/components/ScriptForm/index.tsx b/src/renderer/components/ScriptForm/index.tsx index ad093b2..0949477 100644 --- a/src/renderer/components/ScriptForm/index.tsx +++ b/src/renderer/components/ScriptForm/index.tsx @@ -1,7 +1,7 @@ /* Fill out fields for a new job and submit it */ -import { Job, Script } from 'shared/types' +import { Job, NameValue, Script, ScriptItemBase } from 'shared/types' import { useState } from 'react' import { useWindowStore } from 'renderer/store' import { @@ -10,7 +10,11 @@ import { getAllRequired, ID, } from 'renderer/utils/utils' -import { restoreJob, runJob } from 'shared/data/slices/pipeline' +import { + requestStylesheetParameters, + restoreJob, + runJob, +} from 'shared/data/slices/pipeline' import { addJob, removeJob, @@ -24,7 +28,7 @@ import { FormField } from '../Fields/FormField' const { App } = window // update the array and return a new copy of it -let updateArrayValue = (value, data, arr) => { +let updateArrayValue = (value: any, data: ScriptItemBase, arr: NameValue[]) => { let arr2 = arr.map((i) => (i.name == data.name ? { ...i, value } : i)) return arr2 } @@ -37,23 +41,62 @@ export function ScriptForm({ job, script }: { job: Job; script: Script }) { let optional = getAllOptional(script) const { settings } = useWindowStore() - let saveValueInJobRequest = (value, data) => { + // for script that have stylesheet parameter available + // the job request must be splitted in two step + // First only display the following parameters + // - inputs, + // - stylesheet, + // - page-width + // - page-height + const isMultistep = optional.findIndex((item) => item.name === 'stylesheet') + if (isMultistep > -1) { + optional = optional.filter((item) => + ['stylesheet', 'page-width', 'page-height'].includes(item.name) + ) + } + // next will send back the partial jobRequest to the backend + // that will sent back a stylesheet-parameters request to the engine + let next = async (e) => { + e.preventDefault() + App.store.dispatch(requestStylesheetParameters(job)) + } + // After requestStylesheetParameters, the engine will return a list of new + // script options. Those are stored separatly in the job.stylesheetParameters + // properties + // When this property is set + if (job.stylesheetParameters != null) { + required = [] + optional = [/*...optional,*/ ...job.stylesheetParameters] + } + + // Allow the user to go back to first inputs and options set + let previous = async (e) => { + e.preventDefault() + App.store.dispatch( + updateJob({ + ...job, + stylesheetParameters: null, + }) + ) + } + + let saveValueInJobRequest = (value: any, item: ScriptItemBase) => { if (!job.jobRequest) { return } let inputs = [...job.jobRequest.inputs] let options = [...job.jobRequest.options] - if (data.mediaType.includes('text/css')) { + if (item.mediaType?.includes('text/css')) { // the css filenames are already formatted by our file widget as 'file:///'... // so i don't think they need to be modified before getting sent to the engine // but this block is a placeholder just in case we have to change it // i haven't tested this on windows as of now } - if (data.kind == 'input') { - inputs = updateArrayValue(value, data, inputs) + if (item.kind == 'input') { + inputs = updateArrayValue(value, item, inputs) } else { - options = updateArrayValue(value, data, options) + options = updateArrayValue(value, item, options) } App.store.dispatch( @@ -151,13 +194,13 @@ export function ScriptForm({ job, script }: { job: Job; script: Script }) {
    {optional.map((item, idx) => - item.mediaType.includes( + item.mediaType?.includes( 'application/vnd.pipeline.tts-config+xml' ) ? ( '' // skip it, we don't need to provide a visual field for this option, it's set globally ) : (
  • - {item.mediaType.includes( + {item.mediaType?.includes( 'text/css' ) ? (
    - + {job.stylesheetParameters != null && ( + + )} + {isMultistep > -1 && + job.stylesheetParameters == null ? ( + + ) : ( + + )}