diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d49eeb..12124c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ## \#2 unreleased +* Added: Support for progress prompt * Added: German translations * Added: Support for assets available in Python diff --git a/src/framework/command_router.ts b/src/framework/command_router.ts index 51124a01..a95b8afb 100644 --- a/src/framework/command_router.ts +++ b/src/framework/command_router.ts @@ -26,7 +26,7 @@ export default class CommandRouter implements CommandHandler { this.bridge.send(command) if (isCommandSystemExit(command)) { - console.log("[CommandRouter] Application exit") + console.log('[CommandRouter] Application exit') } else { resolve({ __type__: 'Response', command, payload: { __type__: 'PayloadVoid', value: undefined } }) } diff --git a/src/framework/processing/py/port/api/props.py b/src/framework/processing/py/port/api/props.py index 435c31c3..77dfef4e 100644 --- a/src/framework/processing/py/port/api/props.py +++ b/src/framework/processing/py/port/api/props.py @@ -167,6 +167,29 @@ def toDict(self): return dict +@dataclass +class PropsUIPromptProgress: + """Prompt the user information during the extraction + + Attributes: + description: text with an explanation + message: can be used to show extraction progress + """ + + description: Translatable + message: str + percentage: Optional[int] = None + + def toDict(self): + dict = {} + dict["__type__"] = "PropsUIPromptProgress" + dict["description"] = self.description.toDict() + dict["message"] = self.message + dict["percentage"] = self.percentage + + return dict + + class RadioItem(TypedDict): """Radio button diff --git a/src/framework/processing/py/port/script.py b/src/framework/processing/py/port/script.py index 17725ba8..776db282 100644 --- a/src/framework/processing/py/port/script.py +++ b/src/framework/processing/py/port/script.py @@ -5,6 +5,7 @@ import pandas as pd import zipfile import json +import time def process(sessionId): @@ -21,11 +22,23 @@ def process(sessionId): promptFile = prompt_file("application/zip, text/plain") fileResult = yield render_donation_page(promptFile) if fileResult.__type__ == 'PayloadString': + # Extracting the zipfile meta_data.append(("debug", f"{key}: extracting file")) - extractionResult = doSomethingWithTheFile(fileResult.value) - if extractionResult != 'invalid': + extraction_result = [] + zipfile_ref = get_zipfile(fileResult.value) + print(zipfile_ref, fileResult.value) + files = get_files(zipfile_ref) + fileCount = len(files) + for index, filename in enumerate(files): + percentage = ((index+1)/fileCount)*100 + promptMessage = prompt_extraction_message(f"Extracting file: {filename}", percentage) + yield render_donation_page(promptMessage) + file_extraction_result = extract_file(zipfile_ref, filename) + extraction_result.append(file_extraction_result) + + if len(extraction_result) >= 0: meta_data.append(("debug", f"{key}: extraction successful, go to consent form")) - data = extractionResult + data = extraction_result break else: meta_data.append(("debug", f"{key}: prompt confirmation to retry file selection")) @@ -88,24 +101,39 @@ def prompt_file(extensions): return props.PropsUIPromptFileInput(description, extensions) +def prompt_extraction_message(message, percentage): + description = props.Translatable({ + "en": "One moment please. Information is now being extracted from the selected file.", + "de": "Einen Moment bitte. Es werden nun Informationen aus der ausgewählten Datei extrahiert.", + "nl": "Een moment geduld. Informatie wordt op dit moment uit het geselecteerde bestaand gehaald." + }) -def doSomethingWithTheFile(filename): - return extract_zip_contents(filename) + return props.PropsUIPromptProgress(description, message, percentage) -def extract_zip_contents(filename): - names = [] +def get_zipfile(filename): try: - file = zipfile.ZipFile(filename) - data = [] - for name in file.namelist(): - names.append(name) - info = file.getinfo(name) - data.append((name, info.compress_size, info.file_size)) - return data + return zipfile.ZipFile(filename) except zipfile.error: return "invalid" + + +def get_files(zipfile_ref): + try: + return zipfile_ref.namelist() + except zipfile.error: + return [] + +def extract_file(zipfile_ref, filename): + try: + # make it slow for demo reasons only + time.sleep(1) + info = zipfile_ref.getinfo(filename) + return (filename, info.compress_size, info.file_size) + except zipfile.error: + return "invalid" + def prompt_consent(data, meta_data): diff --git a/src/framework/processing/worker_engine.ts b/src/framework/processing/worker_engine.ts index d87b80e7..9d4d6a99 100755 --- a/src/framework/processing/worker_engine.ts +++ b/src/framework/processing/worker_engine.ts @@ -1,5 +1,5 @@ import { CommandHandler, ProcessingEngine } from '../types/modules' -import {CommandSystemEvent, isCommand, Response } from '../types/commands' +import { CommandSystemEvent, isCommand, Response } from '../types/commands' export default class WorkerProcessingEngine implements ProcessingEngine { sessionId: String @@ -23,8 +23,8 @@ export default class WorkerProcessingEngine implements ProcessingEngine { } } - sendSystemEvent(name: string): void { - const command: CommandSystemEvent = { __type__: 'CommandSystemEvent', name} + sendSystemEvent (name: string): void { + const command: CommandSystemEvent = { __type__: 'CommandSystemEvent', name } this.commandHandler.onCommand(command).then( () => {}, () => {} @@ -58,9 +58,9 @@ export default class WorkerProcessingEngine implements ProcessingEngine { const waitForInitialization: Promise = this.waitForInitialization() waitForInitialization.then( - () => { - this.sendSystemEvent("initialized") - this.firstRunCycle() + () => { + this.sendSystemEvent('initialized') + this.firstRunCycle() }, () => {} ) diff --git a/src/framework/types/commands.ts b/src/framework/types/commands.ts index 277b91a4..c2b7ae26 100644 --- a/src/framework/types/commands.ts +++ b/src/framework/types/commands.ts @@ -87,7 +87,7 @@ export type CommandSystem = CommandSystemExit export function isCommandSystem (arg: any): arg is CommandSystem { - return isCommandSystemDonate(arg) || isCommandSystemEvent(arg) || isCommandSystemExit(arg) + return isCommandSystemDonate(arg) || isCommandSystemEvent(arg) || isCommandSystemExit(arg) } export interface CommandSystemEvent { diff --git a/src/framework/types/elements.ts b/src/framework/types/elements.ts index 0e6f33dc..36ffcd41 100644 --- a/src/framework/types/elements.ts +++ b/src/framework/types/elements.ts @@ -9,7 +9,7 @@ export type PropsUI = PropsUICheckBox | PropsUIRadioItem | PropsUISpinner | - PropsUIProgress | + PropsUIProgressBar | PropsUIHeader | PropsUITable | PropsUISearchBar | @@ -310,17 +310,17 @@ export function isPropsUISpinner (arg: any): arg is PropsUISpinner { return isInstanceOf(arg, 'PropsUISpinner', ['color', 'spinning']) } -// PROGRESS +// PROGRESS BAR -export interface PropsUIProgress { - __type__: 'PropsUIProgress' +export interface PropsUIProgressBar { + __type__: 'PropsUIProgressBar' percentage: number } -export function isPropsUIProgress (arg: any): arg is PropsUIProgress { - return isInstanceOf(arg, 'PropsUIProgress', ['percentage']) +export function isPropsUIProgress (arg: any): arg is PropsUIProgressBar { + return isInstanceOf(arg, 'PropsUIProgressBar', ['percentage']) } -// Header +// HEADER export interface PropsUIHeader { __type__: 'PropsUIHeader' diff --git a/src/framework/types/pages.ts b/src/framework/types/pages.ts index c91136c7..baf07433 100644 --- a/src/framework/types/pages.ts +++ b/src/framework/types/pages.ts @@ -1,6 +1,6 @@ import { isInstanceOf } from '../helpers' import { PropsUIHeader } from './elements' -import { PropsUIPromptFileInput, PropsUIPromptConfirm, PropsUIPromptConsentForm, PropsUIPromptRadioInput } from './prompts' +import { PropsUIPromptFileInput, PropsUIPromptProgress, PropsUIPromptConfirm, PropsUIPromptConsentForm, PropsUIPromptRadioInput } from './prompts' export type PropsUIPage = PropsUIPageSplashScreen | @@ -22,7 +22,7 @@ export interface PropsUIPageDonation { __type__: 'PropsUIPageDonation' platform: string header: PropsUIHeader - body: PropsUIPromptFileInput | PropsUIPromptConfirm | PropsUIPromptConsentForm | PropsUIPromptRadioInput + body: PropsUIPromptFileInput | PropsUIPromptProgress | PropsUIPromptConfirm | PropsUIPromptConsentForm | PropsUIPromptRadioInput } export function isPropsUIPageDonation (arg: any): arg is PropsUIPageDonation { return isInstanceOf(arg, 'PropsUIPageDonation', ['platform', 'header', 'body']) diff --git a/src/framework/types/prompts.ts b/src/framework/types/prompts.ts index d730da42..a8a1fdc4 100644 --- a/src/framework/types/prompts.ts +++ b/src/framework/types/prompts.ts @@ -3,6 +3,7 @@ import { PropsUIRadioItem, Text } from './elements' export type PropsUIPrompt = PropsUIPromptFileInput | + PropsUIPromptProgress | PropsUIPromptRadioInput | PropsUIPromptConsentForm | PropsUIPromptConfirm @@ -32,6 +33,16 @@ export function isPropsUIPromptFileInput (arg: any): arg is PropsUIPromptFileInp return isInstanceOf(arg, 'PropsUIPromptFileInput', ['description', 'extensions']) } +export interface PropsUIPromptProgress { + __type__: 'PropsUIPromptProgress' + description: Text + message: string + percentage?: number +} +export function isPropsUIPromptProgress (arg: any): arg is PropsUIPromptProgress { + return isInstanceOf(arg, 'PropsUIPromptProgress', ['description', 'message']) +} + export interface PropsUIPromptRadioInput { __type__: 'PropsUIPromptRadioInput' title: Text diff --git a/src/framework/visualisation/react/ui/elements/progress.tsx b/src/framework/visualisation/react/ui/elements/progress_bar.tsx similarity index 68% rename from src/framework/visualisation/react/ui/elements/progress.tsx rename to src/framework/visualisation/react/ui/elements/progress_bar.tsx index 38aae053..e3636506 100644 --- a/src/framework/visualisation/react/ui/elements/progress.tsx +++ b/src/framework/visualisation/react/ui/elements/progress_bar.tsx @@ -1,9 +1,9 @@ import { Weak } from '../../../../helpers' -import { PropsUIProgress } from '../../../../types/elements' +import { PropsUIProgressBar } from '../../../../types/elements' -type Props = Weak +type Props = Weak -export const Progress = ({ percentage }: Props): JSX.Element => { +export const ProgressBar = ({ percentage }: Props): JSX.Element => { return (
diff --git a/src/framework/visualisation/react/ui/pages/donation_page.tsx b/src/framework/visualisation/react/ui/pages/donation_page.tsx index 3eb3cf89..d1641d93 100644 --- a/src/framework/visualisation/react/ui/pages/donation_page.tsx +++ b/src/framework/visualisation/react/ui/pages/donation_page.tsx @@ -4,12 +4,13 @@ import TextBundle from '../../../../text_bundle' import { Translator } from '../../../../translator' import { Translatable } from '../../../../types/elements' import { PropsUIPageDonation } from '../../../../types/pages' -import { isPropsUIPromptConfirm, isPropsUIPromptConsentForm, isPropsUIPromptFileInput, isPropsUIPromptRadioInput } from '../../../../types/prompts' +import { isPropsUIPromptConfirm, isPropsUIPromptConsentForm, isPropsUIPromptFileInput, isPropsUIPromptProgress, isPropsUIPromptRadioInput } from '../../../../types/prompts' import { ReactFactoryContext } from '../../factory' import { Title1 } from '../elements/text' import { Confirm } from '../prompts/confirm' import { ConsentForm } from '../prompts/consent_form' import { FileInput } from '../prompts/file_input' +import { Progress } from '../prompts/progress' import { RadioInput } from '../prompts/radio_input' import { Page } from './templates/page' @@ -25,6 +26,9 @@ export const DonationPage = (props: Props): JSX.Element => { if (isPropsUIPromptFileInput(body)) { return } + if (isPropsUIPromptProgress(body)) { + return + } if (isPropsUIPromptConfirm(body)) { return } diff --git a/src/framework/visualisation/react/ui/prompts/consent_form.tsx b/src/framework/visualisation/react/ui/prompts/consent_form.tsx index 17f10a41..6aee8597 100644 --- a/src/framework/visualisation/react/ui/prompts/consent_form.tsx +++ b/src/framework/visualisation/react/ui/prompts/consent_form.tsx @@ -24,7 +24,7 @@ export const ConsentForm = (props: Props): JSX.Element => { const [waiting, setWaiting] = React.useState(false) const { locale, resolve } = props - const cancelButton = Translator.translate(cancelButtonLabel, props.locale) + const cancelButton = Translator.translate(cancelButtonLabel, props.locale) function rowCell (dataFrame: any, column: string, row: number): PropsUITableCell { const text = String(dataFrame[column][`${row}`]) @@ -157,8 +157,10 @@ export const ConsentForm = (props: Props): JSX.Element => {
- +
diff --git a/src/framework/visualisation/react/ui/prompts/progress.tsx b/src/framework/visualisation/react/ui/prompts/progress.tsx new file mode 100644 index 00000000..dced309e --- /dev/null +++ b/src/framework/visualisation/react/ui/prompts/progress.tsx @@ -0,0 +1,61 @@ +import { Weak } from '../../../../helpers' +import { Translator } from '../../../../translator' +import { ReactFactoryContext } from '../../factory' +import { PropsUIPromptProgress } from '../../../../types/prompts' +import { ProgressBar } from '../elements/progress_bar' + +type Props = Weak & ReactFactoryContext + +export const Progress = (props: Props): JSX.Element => { + const { resolve, percentage } = props + const { description, message } = prepareCopy(props) + + function progressBar (): JSX.Element { + if (percentage !== undefined) { + return ( + <> +
+ + + ) + } else { + return <> + } + } + + function autoResolve (): void { + resolve?.({ __type__: 'PayloadTrue', value: true }) + } + + // No user action possible, resolve directly to give control back to script + autoResolve() + + return ( + <> +
+
+ {description} +
+
+
+
+ {message} +
+ {progressBar()} +
+
+ + ) +} + +interface Copy { + description: string + message: string +} + +function prepareCopy ({ description, message, locale }: Props): Copy { + return { + description: Translator.translate(description, locale), + message: message + } +} diff --git a/src/index.tsx b/src/index.tsx index d24276c2..17e85721 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -29,9 +29,9 @@ if (process.env.REACT_APP_BUILD !== 'standalone' && process.env.NODE_ENV === 'pr } const observer = new ResizeObserver(() => { - const height = window.document.body.scrollHeight; - const action = "resize" - window.parent.postMessage({action, height}, "*") -}); + const height = window.document.body.scrollHeight + const action = 'resize' + window.parent.postMessage({ action, height }, '*') +}) -observer.observe(window.document.body); \ No newline at end of file +observer.observe(window.document.body) diff --git a/src/live_bridge.ts b/src/live_bridge.ts index b50b696d..e03431b4 100644 --- a/src/live_bridge.ts +++ b/src/live_bridge.ts @@ -32,6 +32,6 @@ export default class LiveBridge implements Bridge { private log (level: 'info' | 'error', ...message: any[]): void { const logger = level === 'info' ? console.log : console.error - logger("[LiveBridge]", ...message) + logger('[LiveBridge]', ...message) } }