├── .alfred.toml ├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .github └── workflows │ ├── ci-e2e.yml │ ├── ci-macos.yml │ ├── ci-ui-test.yml │ ├── ci-windows.yml │ ├── ci.yml │ ├── publish.yml │ └── trigger-workflow.yml ├── .gitignore ├── .nvmrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── alfred ├── apps.py ├── build.py ├── ci.py ├── github.py ├── install.py ├── npm.py ├── publish.py └── run.py ├── apps ├── default │ ├── .wf │ │ ├── components-blueprints_blueprint-0-t84xyhxau9ej3823.jsonl │ │ ├── components-blueprints_root.jsonl │ │ ├── components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl │ │ ├── components-root.jsonl │ │ └── metadata.json │ ├── main.py │ └── static │ │ ├── README.md │ │ ├── favicon.png │ │ └── welcome.svg └── hello │ ├── .wf │ ├── components-blueprints_blueprint-0-t84xyhxau9ej3823.jsonl │ ├── components-blueprints_root.jsonl │ ├── components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl │ ├── components-root.jsonl │ └── metadata.json │ ├── main.py │ └── static │ ├── README.md │ ├── favicon.png │ └── welcome.svg ├── favicon.svg ├── mypy.ini ├── package-lock.json ├── package.json ├── poetry.lock ├── poetry.toml ├── pyproject.toml ├── pytest.ini ├── src ├── ui │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierrc │ ├── .storybook │ │ ├── main.ts │ │ └── preview.ts │ ├── index.html │ ├── package.json │ ├── public │ │ ├── components │ │ │ ├── annotatedtext.svg │ │ │ ├── avatar.svg │ │ │ ├── blueprints_addtostatelist.svg │ │ │ ├── blueprints_calleventhandler.svg │ │ │ ├── blueprints_category_Logic.svg │ │ │ ├── blueprints_category_Other.svg │ │ │ ├── blueprints_category_Triggers.svg │ │ │ ├── blueprints_category_Writer.svg │ │ │ ├── blueprints_code.svg │ │ │ ├── blueprints_foreach.svg │ │ │ ├── blueprints_httprequest.svg │ │ │ ├── blueprints_logmessage.svg │ │ │ ├── blueprints_parsejson.svg │ │ │ ├── blueprints_returnvalue.svg │ │ │ ├── blueprints_runblueprint.svg │ │ │ ├── blueprints_setstate.svg │ │ │ ├── blueprints_uieventtrigger.svg │ │ │ ├── blueprints_writeraddchatmessage.svg │ │ │ ├── blueprints_writeraddtokg.svg │ │ │ ├── blueprints_writerchat.svg │ │ │ ├── blueprints_writerclassification.svg │ │ │ ├── blueprints_writercompletion.svg │ │ │ ├── blueprints_writerinitchat.svg │ │ │ ├── blueprints_writernocodeapp.svg │ │ │ ├── button.svg │ │ │ ├── category_Content.svg │ │ │ ├── category_Embed.svg │ │ │ ├── category_Input.svg │ │ │ ├── category_Layout.svg │ │ │ ├── category_Other.svg │ │ │ ├── chatbot.svg │ │ │ ├── checkboxinput.svg │ │ │ ├── colorinput.svg │ │ │ ├── column.svg │ │ │ ├── columns.svg │ │ │ ├── dataframe.svg │ │ │ ├── dateinput.svg │ │ │ ├── dropdowninput.svg │ │ │ ├── fileinput.svg │ │ │ ├── googlemaps.svg │ │ │ ├── header.svg │ │ │ ├── heading.svg │ │ │ ├── horizontalstack.svg │ │ │ ├── html.svg │ │ │ ├── icon.svg │ │ │ ├── iframe.svg │ │ │ ├── image.svg │ │ │ ├── jsonviewer.svg │ │ │ ├── link.svg │ │ │ ├── mapbox.svg │ │ │ ├── message.svg │ │ │ ├── metric.svg │ │ │ ├── multiselectinput.svg │ │ │ ├── numberinput.svg │ │ │ ├── page.svg │ │ │ ├── pagination.svg │ │ │ ├── pdf.svg │ │ │ ├── plotlygraph.svg │ │ │ ├── progressbar.svg │ │ │ ├── radioinput.svg │ │ │ ├── rangeinput.svg │ │ │ ├── ratinginput.svg │ │ │ ├── repeater.svg │ │ │ ├── reuse.svg │ │ │ ├── section.svg │ │ │ ├── selectinput.svg │ │ │ ├── separator.svg │ │ │ ├── sidebar.svg │ │ │ ├── sliderinput.svg │ │ │ ├── step.svg │ │ │ ├── steps.svg │ │ │ ├── switchinput.svg │ │ │ ├── tab.svg │ │ │ ├── tabs.svg │ │ │ ├── tags.svg │ │ │ ├── text.svg │ │ │ ├── textareainput.svg │ │ │ ├── textinput.svg │ │ │ ├── timeinput.svg │ │ │ ├── timer.svg │ │ │ ├── vegalitechart.svg │ │ │ ├── videoplayer.svg │ │ │ └── webcamcapture.svg │ │ ├── favicon.png │ │ └── status │ │ │ ├── error.svg │ │ │ └── success.svg │ ├── src │ │ ├── ambientTypes.ts │ │ ├── assets │ │ │ ├── art-paper.svg │ │ │ ├── file-drop-placeholder.svg │ │ │ ├── logo.svg │ │ │ ├── note-active.svg │ │ │ ├── note-cursor.svg │ │ │ ├── note-default.svg │ │ │ ├── note-hover.svg │ │ │ ├── note-new.svg │ │ │ ├── padding-4-side.svg │ │ │ ├── padding-bottom-side.svg │ │ │ ├── padding-left-side.svg │ │ │ ├── padding-right-side.svg │ │ │ ├── padding-top-side.svg │ │ │ ├── padding-x-side.svg │ │ │ └── padding-y-side.svg │ │ ├── builder │ │ │ ├── BuilderApp.vue │ │ │ ├── BuilderApplicationSelect.vue │ │ │ ├── BuilderAsyncLoader.vue │ │ │ ├── BuilderBlueprintState.vue │ │ │ ├── BuilderCollaborationTracker.vue │ │ │ ├── BuilderCopyText.vue │ │ │ ├── BuilderDropFileZone.vue │ │ │ ├── BuilderEmbeddedCodeEditor.vue │ │ │ ├── BuilderGraphSelect.spec.ts │ │ │ ├── BuilderGraphSelect.vue │ │ │ ├── BuilderHeader.spec.ts │ │ │ ├── BuilderHeader.vue │ │ │ ├── BuilderHeaderConnected.vue │ │ │ ├── BuilderInsertionLabel.vue │ │ │ ├── BuilderInsertionOverlay.vue │ │ │ ├── BuilderInstanceTracker.vue │ │ │ ├── BuilderListItem.vue │ │ │ ├── BuilderModelSelect.vue │ │ │ ├── BuilderStateExplorer.vue │ │ │ ├── BuilderSwitcher.vue │ │ │ ├── BuilderToasts.vue │ │ │ ├── BuilderTooltip.vue │ │ │ ├── BuilderTree.vue │ │ │ ├── builderEditorWorker.ts │ │ │ ├── builderManager.spec.ts │ │ │ ├── builderManager.ts │ │ │ ├── ico.css │ │ │ ├── panels │ │ │ │ ├── BuilderCodePanel.vue │ │ │ │ ├── BuilderCodePanelFileUploadBtn.vue │ │ │ │ ├── BuilderCodePanelFileUploading.vue │ │ │ │ ├── BuilderCodePanelSourceFilesTree.vue │ │ │ │ ├── BuilderLogBlueprintExecution.vue │ │ │ │ ├── BuilderLogBlueprintExecutionTrace.vue │ │ │ │ ├── BuilderLogIndicator.vue │ │ │ │ ├── BuilderLogPanel.vue │ │ │ │ ├── BuilderPanel.vue │ │ │ │ └── BuilderPanelSwitcher.vue │ │ │ ├── settings │ │ │ │ ├── BuilderFieldsAlign.vue │ │ │ │ ├── BuilderFieldsBlueprintKey.spec.ts │ │ │ │ ├── BuilderFieldsBlueprintKey.vue │ │ │ │ ├── BuilderFieldsCode.vue │ │ │ │ ├── BuilderFieldsColor.vue │ │ │ │ ├── BuilderFieldsComponentEventType.vue │ │ │ │ ├── BuilderFieldsComponentId.spec.ts │ │ │ │ ├── BuilderFieldsComponentId.vue │ │ │ │ ├── BuilderFieldsHandler.spec.ts │ │ │ │ ├── BuilderFieldsHandler.vue │ │ │ │ ├── BuilderFieldsKeyValue.vue │ │ │ │ ├── BuilderFieldsKeyValueModal.vue │ │ │ │ ├── BuilderFieldsObject.vue │ │ │ │ ├── BuilderFieldsPadding.vue │ │ │ │ ├── BuilderFieldsShadow.vue │ │ │ │ ├── BuilderFieldsText.vue │ │ │ │ ├── BuilderFieldsTools.vue │ │ │ │ ├── BuilderFieldsWidth.vue │ │ │ │ ├── BuilderFieldsWriterResourceId.vue │ │ │ │ ├── BuilderSectionTitle.vue │ │ │ │ ├── BuilderSettings.vue │ │ │ │ ├── BuilderSettingsAPICode.vue │ │ │ │ ├── BuilderSettingsActions.vue │ │ │ │ ├── BuilderSettingsBinding.vue │ │ │ │ ├── BuilderSettingsHandlers.vue │ │ │ │ ├── BuilderSettingsHandlersBlueprint.vue │ │ │ │ ├── BuilderSettingsMain.vue │ │ │ │ ├── BuilderSettingsProperties.spec.ts │ │ │ │ ├── BuilderSettingsProperties.vue │ │ │ │ ├── BuilderSettingsVisibility.vue │ │ │ │ ├── BuilderTemplateInput.vue │ │ │ │ ├── composables │ │ │ │ │ ├── useKeyValueEditor.spec.ts │ │ │ │ │ └── useKeyValueEditor.ts │ │ │ │ └── constants │ │ │ │ │ └── builderFieldsCssTabs.ts │ │ │ ├── sharedStyles.css │ │ │ ├── sidebar │ │ │ │ ├── BuilderSidebar.vue │ │ │ │ ├── BuilderSidebarButton.vue │ │ │ │ ├── BuilderSidebarComponentTree.vue │ │ │ │ ├── BuilderSidebarComponentTreeBranch.vue │ │ │ │ ├── BuilderSidebarNote.vue │ │ │ │ ├── BuilderSidebarNoteForm.vue │ │ │ │ ├── BuilderSidebarNotes.vue │ │ │ │ ├── BuilderSidebarNotesEmpty.vue │ │ │ │ ├── BuilderSidebarPanel.vue │ │ │ │ ├── BuilderSidebarToolkit.vue │ │ │ │ └── composables │ │ │ │ │ ├── useComponentsTreeSearch.spec.ts │ │ │ │ │ └── useComponentsTreeSearch.ts │ │ │ ├── useComponentAction.spec.ts │ │ │ ├── useComponentActions.ts │ │ │ ├── useComponentClipboard.ts │ │ │ ├── useComponentDescription.ts │ │ │ ├── useDragDropComponent.ts │ │ │ ├── useToast.ts │ │ │ └── useWriterAppDeployment.ts │ │ ├── components │ │ │ ├── blueprints │ │ │ │ ├── BlueprintsAutogen.vue │ │ │ │ ├── BlueprintsBlueprint.vue │ │ │ │ ├── BlueprintsGenerationLoader.vue │ │ │ │ ├── BlueprintsLifeLoading.vue │ │ │ │ ├── BlueprintsRoot.vue │ │ │ │ ├── GradientCircle.vue │ │ │ │ ├── abstract │ │ │ │ │ └── BlueprintsNode.vue │ │ │ │ └── base │ │ │ │ │ ├── BlueprintArrow.vue │ │ │ │ │ ├── BlueprintMiniMap.vue │ │ │ │ │ ├── BlueprintNavigator.vue │ │ │ │ │ ├── BlueprintToolbar.vue │ │ │ │ │ ├── BlueprintToolbarBlocksDropdown.vue │ │ │ │ │ └── BlueprintsNodeNamer.vue │ │ │ ├── core │ │ │ │ ├── base │ │ │ │ │ ├── BaseCollapseButton.vue │ │ │ │ │ ├── BaseContainer.vue │ │ │ │ │ ├── BaseEmptiness.vue │ │ │ │ │ ├── BaseInputColor.vue │ │ │ │ │ ├── BaseInputSlider.utils.ts │ │ │ │ │ ├── BaseInputSlider.vue │ │ │ │ │ ├── BaseInputSliderLayout.vue │ │ │ │ │ ├── BaseInputSliderRange.vue │ │ │ │ │ ├── BaseInputSliderThumb.vue │ │ │ │ │ ├── BaseInputWrapper.vue │ │ │ │ │ ├── BaseMarkdown.vue │ │ │ │ │ ├── BaseMarkdownRaw.vue │ │ │ │ │ ├── BaseNote.vue │ │ │ │ │ └── BaseTransitionSlideFade.vue │ │ │ │ ├── content │ │ │ │ │ ├── CoreAnnotatedText.spec.ts │ │ │ │ │ ├── CoreAnnotatedText.vue │ │ │ │ │ ├── CoreAvatar.vue │ │ │ │ │ ├── CoreChatBot │ │ │ │ │ │ ├── CoreChatbotAvatar.vue │ │ │ │ │ │ ├── CoreChatbotMessage.vue │ │ │ │ │ │ └── CoreChatbotSentMessageIcon.vue │ │ │ │ │ ├── CoreChatbot.vue │ │ │ │ │ ├── CoreDataframe.vue │ │ │ │ │ ├── CoreDataframe │ │ │ │ │ │ ├── CoreDataframeCell.vue │ │ │ │ │ │ ├── CoreDataframeCellBoolean.vue │ │ │ │ │ │ ├── CoreDataframeCellNumber.vue │ │ │ │ │ │ ├── CoreDataframeCellText.vue │ │ │ │ │ │ ├── CoreDataframeCellUnknown.vue │ │ │ │ │ │ ├── CoreDataframeRow.vue │ │ │ │ │ │ ├── constants.ts │ │ │ │ │ │ ├── useDataframeValueBroker.ts │ │ │ │ │ │ └── useJobs.ts │ │ │ │ │ ├── CoreHeading.vue │ │ │ │ │ ├── CoreIcon.vue │ │ │ │ │ ├── CoreImage.vue │ │ │ │ │ ├── CoreJsonViewer.vue │ │ │ │ │ ├── CoreLink.vue │ │ │ │ │ ├── CoreMessage.vue │ │ │ │ │ ├── CoreMetric.vue │ │ │ │ │ ├── CorePlotlyGraph.vue │ │ │ │ │ ├── CoreProgressBar.vue │ │ │ │ │ ├── CoreTags.vue │ │ │ │ │ ├── CoreText.spec.ts │ │ │ │ │ ├── CoreText.vue │ │ │ │ │ ├── CoreVegaLiteChart.vue │ │ │ │ │ ├── CoreVideoPlayer.vue │ │ │ │ │ └── __snapshots__ │ │ │ │ │ │ ├── CoreAnnotatedText.spec.ts.snap │ │ │ │ │ │ └── CoreText.spec.ts.snap │ │ │ │ ├── embed │ │ │ │ │ ├── CoreGoogleMaps.vue │ │ │ │ │ ├── CoreIFrame.vue │ │ │ │ │ ├── CoreMapbox.vue │ │ │ │ │ └── CorePDF.vue │ │ │ │ ├── input │ │ │ │ │ ├── CoreCheckboxInput.spec.ts │ │ │ │ │ ├── CoreCheckboxInput.vue │ │ │ │ │ ├── CoreColorInput.vue │ │ │ │ │ ├── CoreDateInput.spec.ts │ │ │ │ │ ├── CoreDateInput.vue │ │ │ │ │ ├── CoreDropdownInput.vue │ │ │ │ │ ├── CoreFileInput.vue │ │ │ │ │ ├── CoreMultiselectInput.vue │ │ │ │ │ ├── CoreNumberInput.vue │ │ │ │ │ ├── CoreRadioInput.spec.ts │ │ │ │ │ ├── CoreRadioInput.vue │ │ │ │ │ ├── CoreRatingInput.vue │ │ │ │ │ ├── CoreSelectInput.vue │ │ │ │ │ ├── CoreSliderInput.vue │ │ │ │ │ ├── CoreSliderRangeInput.vue │ │ │ │ │ ├── CoreSwitchInput.vue │ │ │ │ │ ├── CoreTextInput.vue │ │ │ │ │ ├── CoreTextareaInput.vue │ │ │ │ │ ├── CoreTimeInput.vue │ │ │ │ │ └── __snapshots__ │ │ │ │ │ │ ├── CoreCheckboxInput.spec.ts.snap │ │ │ │ │ │ ├── CoreDateInput.spec.ts.snap │ │ │ │ │ │ └── CoreRadioInput.spec.ts.snap │ │ │ │ ├── internal │ │ │ │ │ └── CoreNote.vue │ │ │ │ ├── layout │ │ │ │ │ ├── CoreColumn.vue │ │ │ │ │ ├── CoreColumns.vue │ │ │ │ │ ├── CoreHeader.vue │ │ │ │ │ ├── CoreHorizontalStack.vue │ │ │ │ │ ├── CoreSection.vue │ │ │ │ │ ├── CoreSeparator.vue │ │ │ │ │ ├── CoreSidebar.vue │ │ │ │ │ ├── CoreStep.vue │ │ │ │ │ ├── CoreSteps.vue │ │ │ │ │ ├── CoreTab.vue │ │ │ │ │ └── CoreTabs.vue │ │ │ │ ├── other │ │ │ │ │ ├── CoreButton.spec.ts │ │ │ │ │ ├── CoreButton.vue │ │ │ │ │ ├── CoreHtml.spec.ts │ │ │ │ │ ├── CoreHtml.vue │ │ │ │ │ ├── CorePagination.vue │ │ │ │ │ ├── CoreRepeater.vue │ │ │ │ │ ├── CoreReuse.vue │ │ │ │ │ ├── CoreTimer.vue │ │ │ │ │ ├── CoreWebcamCapture.vue │ │ │ │ │ └── __snapshots__ │ │ │ │ │ │ └── CoreHtml.spec.ts.snap │ │ │ │ └── root │ │ │ │ │ ├── CorePage.vue │ │ │ │ │ └── CoreRoot.vue │ │ │ ├── custom │ │ │ │ ├── BubbleMessage.vue │ │ │ │ ├── BubbleMessageAdvanced.vue │ │ │ │ └── index.ts │ │ │ └── shared │ │ │ │ ├── README.md │ │ │ │ ├── ShareHorizontalResize.vue │ │ │ │ ├── SharedButtonCopyClipboard.vue │ │ │ │ ├── SharedCollaborationCanvas.vue │ │ │ │ ├── SharedCollapsible.vue │ │ │ │ ├── SharedControlBar.vue │ │ │ │ ├── SharedImgWithFallback.spec.ts │ │ │ │ ├── SharedImgWithFallback.vue │ │ │ │ ├── SharedJsonViewer │ │ │ │ ├── SharedJsonViewer.spec.ts │ │ │ │ ├── SharedJsonViewer.utils.ts │ │ │ │ ├── SharedJsonViewer.vue │ │ │ │ ├── SharedJsonViewerChildrenCounter.vue │ │ │ │ ├── SharedJsonViewerCollapsible.vue │ │ │ │ ├── SharedJsonViewerObject.spec.ts │ │ │ │ ├── SharedJsonViewerObject.vue │ │ │ │ ├── SharedJsonViewerValue.vue │ │ │ │ └── __snapshots__ │ │ │ │ │ └── SharedJsonViewer.spec.ts.snap │ │ │ │ ├── SharedMoreDropdown.vue │ │ │ │ └── SharedWriterAvatar.vue │ │ ├── composables │ │ │ ├── useAbortController.ts │ │ │ ├── useAssetContentType.spec.ts │ │ │ ├── useAssetContentType.ts │ │ │ ├── useBlueprintRun.ts │ │ │ ├── useBoundingClientRect.ts │ │ │ ├── useCollaborationManager.ts │ │ │ ├── useComponentBlueprints.ts │ │ │ ├── useDebouncer.spec.ts │ │ │ ├── useDebouncer.ts │ │ │ ├── useFocusWithin.ts │ │ │ ├── useFormatter.ts │ │ │ ├── useListResources.spec.ts │ │ │ ├── useListResources.ts │ │ │ ├── useLocalStorageJSON.spec.ts │ │ │ ├── useLocalStorageJSON.ts │ │ │ ├── useLogger.ts │ │ │ ├── useWriterApi.ts │ │ │ ├── useWriterApiUser.ts │ │ │ ├── useWriterTracking.spec.ts │ │ │ └── useWriterTracking.ts │ │ ├── constants │ │ │ ├── component.ts │ │ │ ├── icons.ts │ │ │ ├── validator.spec.ts │ │ │ └── validators.ts │ │ ├── core │ │ │ ├── auditAndFix.ts │ │ │ ├── detectPlatform.ts │ │ │ ├── index.ts │ │ │ ├── loadExtensions.ts │ │ │ ├── navigation.spec.ts │ │ │ ├── navigation.ts │ │ │ ├── parsing.ts │ │ │ ├── serializer.ts │ │ │ ├── sourceFiles.spec.ts │ │ │ ├── sourceFiles.ts │ │ │ ├── templateMap.ts │ │ │ ├── typeHierarchy.ts │ │ │ ├── useNotesManager.spec.ts │ │ │ ├── useNotesManager.ts │ │ │ ├── useSourceFiles.spec.ts │ │ │ └── useSourceFiles.ts │ │ ├── directives.ts │ │ ├── fonts.ts │ │ ├── injectionKeys.ts │ │ ├── main.ts │ │ ├── renderer │ │ │ ├── ChildlessPlaceholder.vue │ │ │ ├── ComponentProxy.vue │ │ │ ├── ComponentRenderer.vue │ │ │ ├── LoadingSymbol.vue │ │ │ ├── RenderError.vue │ │ │ ├── RendererNotifications.vue │ │ │ ├── colorTransformations.css │ │ │ ├── instancePath.ts │ │ │ ├── sharedStyleFields.ts │ │ │ ├── sharedStyles.css │ │ │ ├── syntheticEvents.ts │ │ │ ├── useEvaluator.ts │ │ │ ├── useFieldsErrors.spec.ts │ │ │ ├── useFieldsErrors.ts │ │ │ └── useFormValueBroker.ts │ │ ├── stories │ │ │ └── fakeCore.ts │ │ ├── tests │ │ │ └── mocks.ts │ │ ├── utils │ │ │ ├── base64.ts │ │ │ ├── geometry.spec.ts │ │ │ ├── geometry.ts │ │ │ ├── math.ts │ │ │ ├── url.spec.ts │ │ │ └── url.ts │ │ ├── wds │ │ │ ├── WdsButton.vue │ │ │ ├── WdsButtonLink.vue │ │ │ ├── WdsButtonSplit.vue │ │ │ ├── WdsCheckbox.vue │ │ │ ├── WdsControl.vue │ │ │ ├── WdsDropdownInput.vue │ │ │ ├── WdsDropdownMenu.spec.ts │ │ │ ├── WdsDropdownMenu.vue │ │ │ ├── WdsFieldWrapper.vue │ │ │ ├── WdsLoaderDots.vue │ │ │ ├── WdsModal.vue │ │ │ ├── WdsNumberInput.vue │ │ │ ├── WdsProgressLinear.vue │ │ │ ├── WdsSelect.spec.ts │ │ │ ├── WdsSelect.vue │ │ │ ├── WdsSkeletonLoader.vue │ │ │ ├── WdsStateDot.vue │ │ │ ├── WdsTab.vue │ │ │ ├── WdsTabs.vue │ │ │ ├── WdsTag.vue │ │ │ ├── WdsTextInput.vue │ │ │ ├── WdsTextareaInput.vue │ │ │ ├── WdsToast.vue │ │ │ └── tokens.ts │ │ ├── writerApi.ts │ │ └── writerTypes.ts │ ├── tools │ │ ├── core.mjs │ │ ├── custom_check.mjs │ │ ├── generator.mjs │ │ ├── generator_python_ui.mjs │ │ ├── generator_storybook.mjs │ │ ├── generator_ui_component_json.mjs │ │ └── getComponents.ts │ ├── tsconfig.json │ ├── vite-env.d.ts │ ├── vite.config.custom.ts │ ├── vite.config.ts │ └── viteWriterPlugin.ts └── writer │ ├── __init__.py │ ├── abstract.py │ ├── ai │ └── __init__.py │ ├── app_runner.py │ ├── audit_and_fix.py │ ├── auth.py │ ├── autogen.py │ ├── blocks │ ├── __init__.py │ ├── addtostatelist.py │ ├── base_block.py │ ├── base_trigger.py │ ├── calleventhandler.py │ ├── changepage.py │ ├── code.py │ ├── foreach.py │ ├── httprequest.py │ ├── logmessage.py │ ├── parsejson.py │ ├── returnvalue.py │ ├── runblueprint.py │ ├── setstate.py │ ├── uieventtrigger.py │ ├── writeraddchatmessage.py │ ├── writeraddtokg.py │ ├── writeraskkg.py │ ├── writerchat.py │ ├── writerclassification.py │ ├── writercompletion.py │ ├── writerfileapi.py │ ├── writerinitchat.py │ ├── writernocodeapp.py │ ├── writerparsepdf.py │ └── writertoolcalling.py │ ├── blueprints.py │ ├── command_line.py │ ├── core.py │ ├── core_df.py │ ├── core_ui.py │ ├── crypto.py │ ├── deploy.py │ ├── evaluator.py │ ├── mypy.ini │ ├── py.typed │ ├── serve.py │ ├── ss_types.py │ ├── sync.py │ ├── templates │ └── auth_unauthorized.html │ ├── ui_manager.py │ └── wf_project.py └── tests ├── backend ├── __init__.py ├── blocks │ ├── conftest.py │ ├── test_addtostatelist.py │ ├── test_base_block.py │ ├── test_calleventhandler.py │ ├── test_changepage.py │ ├── test_code.py │ ├── test_foreach.py │ ├── test_httprequest.py │ ├── test_logmessage.py │ ├── test_parsejson.py │ ├── test_returnvalue.py │ ├── test_runblueprint.py │ ├── test_setstate.py │ ├── test_writeraddchatmessage.py │ ├── test_writeraddtokg.py │ ├── test_writerchat.py │ ├── test_writerclassification.py │ ├── test_writercompletion.py │ ├── test_writerfileapi.py │ ├── test_writerinitchat.py │ ├── test_writernocodeapp.py │ └── test_writerparsepdf.py ├── fixtures │ ├── __init__.py │ ├── app_runner_fixtures.py │ ├── cloud_deploy_fixtures.py │ ├── components │ │ ├── components-page-0.jsonl │ │ ├── components-page-1.jsonl │ │ └── components-root.jsonl │ ├── core_ui_fixtures.py │ ├── file_fixtures.py │ ├── obsoletes │ │ └── ui_obsolete_visible.json │ └── writer_fixtures.py ├── test_ai.py ├── test_app_runner.py ├── test_audit_and_fix.py ├── test_auth.py ├── test_cli.py ├── test_core.py ├── test_core_ui.py ├── test_deploy.py ├── test_evaluator.py ├── test_init_state.py ├── test_middleware.py ├── test_serve.py ├── test_sync.py ├── test_ui.py ├── test_wf_project.py ├── testapp │ ├── .wf │ │ ├── components-blueprints_blueprint-0-hywgzgfetx6rpiqz.jsonl │ │ ├── components-blueprints_blueprint-1-8ffkuce0ermsm9dr.jsonl │ │ ├── components-blueprints_root.jsonl │ │ ├── components-page-0-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl │ │ ├── components-page-1-03796247-dec4-4671-85c9-16559789e013.jsonl │ │ ├── components-page-2-7730df5b-8731-4123-bacc-898e7347b124.jsonl │ │ ├── components-page-3-35986d56-3a1a-4ded-bb5c-b60c2046756f.jsonl │ │ ├── components-page-4-88ea37a5-eb07-4740-ae42-a3eeeacca310.jsonl │ │ ├── components-page-5-28a2212b-bc58-4398-8a72-2554e5296490.jsonl │ │ ├── components-page-6-4b6f14b0-b2d9-43e7-8aba-8d3e939c1f83.jsonl │ │ ├── components-page-7-06c660f8-e3d6-4bdd-92be-64cb080ee933.jsonl │ │ ├── components-page-8-aade8074-13be-4e11-a405-a71b8138e6ae.jsonl │ │ ├── components-root.jsonl │ │ └── metadata.json │ ├── Dockerfile │ ├── assets │ │ ├── main_df.csv │ │ ├── myfile.csv │ │ ├── story.csv │ │ └── story.txt │ ├── main.py │ ├── requirements.txt │ ├── server_setup.py │ └── static │ │ ├── Banana.jpg │ │ ├── Lettuce.jpg │ │ ├── README.md │ │ ├── Spinach.jpg │ │ ├── favicon.png │ │ └── file.js ├── testbasicauth │ ├── .wf │ │ ├── components-blueprints_root.jsonl │ │ ├── components-root.jsonl │ │ └── metadata.json │ ├── main.py │ ├── server_setup.py │ └── static │ │ └── file.js └── testmultiapp │ ├── app1 │ ├── .wf │ │ ├── components-blueprints_root.jsonl │ │ ├── components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl │ │ ├── components-root.jsonl │ │ └── metadata.json │ ├── __init__.py │ ├── main.py │ └── static │ │ └── favicon.png │ └── app2 │ ├── .wf │ ├── components-blueprints_root.jsonl │ ├── components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl │ ├── components-root.jsonl │ └── metadata.json │ ├── __init__.py │ ├── main.py │ └── static │ └── favicon.png ├── conftest.py └── e2e ├── .gitignore ├── index.js ├── package.json ├── playwright.config.ts ├── presets ├── 2columns │ ├── .wf │ │ ├── components-blueprints_root.jsonl │ │ ├── components-page-0-qlbz49xq2emx9ip4.jsonl │ │ ├── components-root.jsonl │ │ └── metadata.json │ └── main.py ├── 2pages │ ├── .wf │ │ ├── components-blueprints_root.jsonl │ │ ├── components-page-0-qlbz49xq2emx9ip4.jsonl │ │ ├── components-page-1-6gnhb317w7k76uhw.jsonl │ │ ├── components-root.jsonl │ │ └── metadata.json │ └── main.py ├── blueprints │ ├── .wf │ │ ├── components-blueprints_blueprint-0-auxjfi7lssb268ly.jsonl │ │ ├── components-blueprints_blueprint-1-n20uom1t17z7c1h8.jsonl │ │ ├── components-blueprints_blueprint-2-bjhk2qqylt0ijn50.jsonl │ │ ├── components-blueprints_root.jsonl │ │ ├── components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl │ │ ├── components-root.jsonl │ │ └── metadata.json │ └── main.py ├── empty_page │ ├── .wf │ │ ├── components-blueprints_root.jsonl │ │ ├── components-page-0-qlbz49xq2emx9ip4.jsonl │ │ ├── components-root.jsonl │ │ └── metadata.json │ └── main.py ├── jsonviewer │ ├── .wf │ │ ├── components-blueprints_root.jsonl │ │ ├── components-page-0-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl │ │ ├── components-root.jsonl │ │ └── metadata.json │ └── main.py ├── low_code │ ├── .wf │ │ ├── components-blueprints_root.jsonl │ │ ├── components-page-0-qlbz49xq2emx9ip4.jsonl │ │ ├── components-root.jsonl │ │ └── metadata.json │ └── main.py ├── section │ ├── .wf │ │ ├── components-blueprints_root.jsonl │ │ ├── components-page-0-qlbz49xq2emx9ip4.jsonl │ │ ├── components-root.jsonl │ │ └── metadata.json │ └── main.py └── state │ ├── .wf │ ├── components-blueprints_root.jsonl │ ├── components-page-0-qlbz49xq2emx9ip4.jsonl │ ├── components-root.jsonl │ └── metadata.json │ └── main.py └── tests ├── blueprints.spec.ts ├── builderFieldValidation.spec.ts ├── button.spec.ts ├── components.spec.ts ├── drag.spec.ts ├── image.spec.ts ├── jsonviewer.spec.ts ├── lowCode.spec.ts ├── reuse.spec.ts ├── sidebar.spec.ts ├── state.spec.ts ├── stateAutocompletion.spec.ts └── undoRedo.spec.ts /.alfred.toml: -------------------------------------------------------------------------------- 1 | [alfred] 2 | # name = "" # name of a subproject, use the name of the directory if not set 3 | # description = "" # inline documentation for a sub project 4 | # subprojects = [] 5 | 6 | # [alfred.project] 7 | # command = [ "alfred/*.py" ] 8 | # python_path_project_root = true 9 | # python_path_extends = [] 10 | # venv = "src/.." 11 | 12 | # more info about project manifest 13 | # into https://alfred-cli.readthedocs.io/en/latest/project.html#setting-up-a-project-with-alfred-toml 14 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "Python 3.11 with Node", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", 7 | "features": { 8 | "ghcr.io/devcontainers/features/node:1": { 9 | "nodeGypDependencies": true, 10 | "version": "lts", 11 | "nvmVersion": "latest" 12 | } 13 | }, 14 | 15 | // Features to add to the dev container. More info: https://containers.dev/features. 16 | // "features": {}, 17 | 18 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 19 | 20 | "forwardPorts": [5173], 21 | "portsAttributes": { 22 | "5173": { 23 | "label": "Vite Dev Server", 24 | "onAutoForward": "openBrowser" 25 | } 26 | } 27 | 28 | // Use 'postCreateCommand' to run commands after the container is created. 29 | // "postCreateCommand": "pip3 install --user -r requirements.txt", 30 | 31 | // Configure tool-specific properties. 32 | // "customizations": {}, 33 | 34 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 35 | // "remoteUser": "root" 36 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | charset = utf-8 8 | indent_style = tab 9 | 10 | [*.py] 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.github/workflows/ci-e2e.yml: -------------------------------------------------------------------------------- 1 | name: ci-e2e 2 | on: 3 | push: 4 | branches: [ dev, master] 5 | pull_request: 6 | branches: [ dev, master] 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest-4-cores 11 | strategy: 12 | matrix: 13 | browser: [ "chromium", "firefox", "webkit" ] 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Install poetry 18 | run: pipx install poetry 19 | 20 | - name: Set up Python 3.11.8 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.11.8" 24 | cache: 'poetry' 25 | 26 | - name: Use Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: "22.x" 30 | cache: npm 31 | 32 | - name: install python3 environment 33 | run: poetry install --with build 34 | 35 | - name: install ci dependencies and generate code 36 | run: poetry run alfred install.ci 37 | 38 | - name: Build UI 39 | run: npm run build 40 | 41 | - name: Install E2E browsers 42 | run: npm run e2e:setup ${{ matrix.browser }} 43 | 44 | - name: Run E2E tests 45 | run: poetry run alfred ci --e2e=${{ matrix.browser }} 46 | -------------------------------------------------------------------------------- /.github/workflows/ci-macos.yml: -------------------------------------------------------------------------------- 1 | name: ci-macos 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | - 'dev' 8 | pull_request: 9 | branches: 10 | - '**' 11 | 12 | jobs: 13 | build: 14 | runs-on: macos-latest-large 15 | strategy: 16 | matrix: 17 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Install poetry 23 | run: pipx install poetry 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | cache: 'poetry' 30 | 31 | - name: Use Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: "22.x" 35 | cache: npm 36 | 37 | - name: install python3 environment 38 | run: poetry install --with build 39 | 40 | - name: install ci dependencies and generate code 41 | run: poetry run alfred install.ci 42 | 43 | - name: run continuous integration pipeline 44 | run: poetry run alfred ci 45 | -------------------------------------------------------------------------------- /.github/workflows/ci-ui-test.yml: -------------------------------------------------------------------------------- 1 | name: ci-ui-test 2 | on: 3 | push: 4 | branches: [dev, master] 5 | paths: ["src/ui/**"] 6 | pull_request: 7 | branches: [dev, master] 8 | paths: ["src/ui/**"] 9 | 10 | jobs: 11 | tests: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: "20.x" 20 | cache: npm 21 | 22 | - name: Install dependencies 23 | run: npm ci -w writer-ui 24 | 25 | - name: Run tests 26 | run: npm test -w writer-ui 27 | -------------------------------------------------------------------------------- /.github/workflows/ci-windows.yml: -------------------------------------------------------------------------------- 1 | name: ci-windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | - 'dev' 8 | pull_request: 9 | branches: 10 | - '**' 11 | 12 | jobs: 13 | build: 14 | runs-on: windows-latest 15 | strategy: 16 | matrix: 17 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Install poetry 23 | run: pipx install poetry 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | cache: 'poetry' 30 | 31 | - name: Use Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: "22.x" 35 | cache: npm 36 | 37 | - name: install python3 environment 38 | run: poetry install --with build 39 | 40 | - name: install ci dependencies and generate code 41 | run: poetry run alfred install.ci 42 | 43 | - name: run continuous integration pipeline 44 | run: poetry run alfred ci 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | - 'dev' 8 | pull_request: 9 | branches: 10 | - '**' 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Install poetry 23 | run: pipx install poetry 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | cache: 'poetry' 30 | 31 | - name: Use Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: "22.x" 35 | cache: npm 36 | 37 | - name: install python3 environment 38 | run: poetry install --with build 39 | 40 | - name: install ci dependencies and generate code 41 | run: poetry run alfred install.ci 42 | 43 | - name: run continuous integration pipeline 44 | run: poetry run alfred ci 45 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | - 'v[0-9]+.[0-9]+.[0-9]+[a-z0-9]*' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Install poetry 18 | run: pipx install poetry 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | cache: 'poetry' 24 | 25 | - name: Use Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: "22.x" 29 | cache: npm 30 | 31 | - name: install python3 environment 32 | run: poetry install --with build 33 | 34 | - name: install ci dependencies and generate code 35 | run: poetry run alfred install.ci 36 | 37 | - name: publish on pypi 38 | run: | 39 | poetry run alfred publish.pypi 40 | env: 41 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/trigger-workflow.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'dev' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Trigger agent manager 17 | run: | 18 | repo_owner="WriterInternal" 19 | repo_name="be.agent-manager" 20 | event_type="trigger-workflow" 21 | commit_sha=${{ github.sha }} 22 | 23 | curl -L \ 24 | -X POST \ 25 | -H "Accept: application/vnd.github+json" \ 26 | -H "Authorization: Bearer ${{ secrets.AGENT_MANAGER_PAT }}" \ 27 | -H "X-GitHub-Api-Version: 2022-11-28" \ 28 | https://api.github.com/repos/$repo_owner/$repo_name/dispatches \ 29 | -d "{\"event_type\": \"$event_type\", \"client_payload\": {\"commit_sha\": \"$commit_sha\"}}" 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.venv/ 2 | **/.winvenv/ 3 | **/.vscode/ 4 | *.pyc 5 | **/__pycache__ 6 | **/.mypy_cache 7 | **/.pytest_cache 8 | **/node_modules/ 9 | docs/docs/.vitepress/cache/ 10 | docs/docs/.vitepress/temp/ 11 | ui/dist/ 12 | build/ 13 | dist/ 14 | src/writer.egg-info/ 15 | *:Zone.Identifier 16 | *.DS_Store 17 | src/writer/static 18 | src/writer/app_templates/* 19 | !src/writer/app_templates/.gitkeep 20 | src/writer/ui.py 21 | src/ui/src/stories/** 22 | src/ui/components.codegen.json 23 | playground/ 24 | *.mp4 25 | .turbo 26 | styles.css 27 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22 2 | -------------------------------------------------------------------------------- /alfred/build.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import shutil 3 | 4 | import alfred 5 | 6 | 7 | @alfred.command("build", help="build apps package for pypi") 8 | @alfred.option("--ignore-ci", is_flag=True, help="ignore continuous integration pipeline") 9 | def build(ignore_ci: bool = False): 10 | if not ignore_ci: 11 | alfred.invoke_command("ci") 12 | else: 13 | alfred.invoke_command("npm.build") 14 | 15 | alfred.invoke_command("build.app_provisionning") 16 | alfred.invoke_command("build.poetry") 17 | 18 | @alfred.command("build.app_provisionning", help="update app templates using ./apps", hidden=True) 19 | def build_app_provisionning(): 20 | if os.path.isdir('src/writer/app_templates'): 21 | shutil.rmtree('src/writer/app_templates') 22 | 23 | shutil.copytree( 'apps', 'src/writer/app_templates') 24 | 25 | @alfred.command("build.poetry", help="build python packages with poetry", hidden=True) 26 | def build_poetry(): 27 | removed_directories = ['dist', 'build'] 28 | for directory in removed_directories: 29 | if os.path.isdir(directory): 30 | shutil.rmtree(directory) 31 | 32 | alfred.run("poetry build") 33 | -------------------------------------------------------------------------------- /alfred/install.py: -------------------------------------------------------------------------------- 1 | import alfred 2 | 3 | 4 | @alfred.command("install.dev", help="install developper dependencies and generate code") 5 | def install_dev(): 6 | alfred.run("poetry install --with build") 7 | alfred.run("npm ci") 8 | alfred.run("npm run build") 9 | 10 | @alfred.command("install.ci", help="install ci dependencies and generate code", hidden=True) 11 | def install_ci(): 12 | alfred.run("npm ci") 13 | alfred.run("npm run build") 14 | -------------------------------------------------------------------------------- /alfred/npm.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import alfred 4 | 5 | 6 | @alfred.command("npm.lint", help="lint check npm packages") 7 | def npm_lint(): 8 | alfred.run("npm run ui:lint.ci") 9 | alfred.run("npm run ui:custom.check") 10 | 11 | @alfred.command("npm.e2e", help="run e2e tests") 12 | @alfred.option('--browser', '-b', help="run e2e tests on specified browser", default='chromium') 13 | def npm_e2e(browser): 14 | with alfred.env(CI="true"): 15 | alfred.run("npm run e2e:"+browser) 16 | 17 | @alfred.command("npm.build", help="build ui code") 18 | def npm_build(): 19 | alfred.run("npm run ui:build") 20 | 21 | @alfred.command("npm.build_custom_components", help="build custom components") 22 | def npm_build_custom_components(): 23 | alfred.run("npm run ui:custom.build") 24 | 25 | @alfred.command("npm.storybook", help="build storybook for continuous integration") 26 | def npm_storybook(): 27 | os.chdir("src/ui") 28 | alfred.run("npm run storybook.build") 29 | 30 | @alfred.command("npm.codegen", help="generate code for low code ui") 31 | def npm_codegen(): 32 | alfred.run("npm run ui:codegen") 33 | -------------------------------------------------------------------------------- /alfred/run.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import alfred 4 | 5 | 6 | @alfred.command("run.storybook", help="preview the storybook as a developper") 7 | def run_storybook(): 8 | os.chdir("src/ui") 9 | alfred.run("npm run storybook") 10 | -------------------------------------------------------------------------------- /apps/default/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /apps/default/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App"}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /apps/default/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.3rc12" 3 | } -------------------------------------------------------------------------------- /apps/default/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | # Shows in the log when the app starts 4 | # print("Hello world!") 5 | 6 | # You can define functions which can be called from Python code blocks 7 | def my_func(): 8 | return 1 9 | 10 | # You can initialize state via code 11 | initial_state = wf.init_state({ 12 | "my_var": 1337, 13 | }) -------------------------------------------------------------------------------- /apps/default/static/README.md: -------------------------------------------------------------------------------- 1 | # Serving static files 2 | 3 | You can use this folder to store files which will be served statically in the "/static" route. 4 | 5 | This is useful to store images and other files which will be served directly to the user of your application. 6 | 7 | For example, if you store an image named "myimage.jpg" in this folder, it'll be accessible as "static/myimage.jpg". 8 | You can use this relative route as the source in an Image component. 9 | -------------------------------------------------------------------------------- /apps/default/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/apps/default/static/favicon.png -------------------------------------------------------------------------------- /apps/hello/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /apps/hello/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App"}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /apps/hello/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.3rc12" 3 | } -------------------------------------------------------------------------------- /apps/hello/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | # Shows in the log when the app starts 4 | # print("Hello world!") 5 | 6 | # You can define functions which can be called from Python code blocks 7 | def my_func(): 8 | return 1 9 | 10 | # You can initialize state via code 11 | initial_state = wf.init_state({ 12 | "my_var": 1337, 13 | }) -------------------------------------------------------------------------------- /apps/hello/static/README.md: -------------------------------------------------------------------------------- 1 | # Serving static files 2 | 3 | You can use this folder to store files which will be served statically in the "/static" route. 4 | 5 | This is useful to store images and other files which will be served directly to the user of your application. 6 | 7 | For example, if you store an image named "myimage.jpg" in this folder, it'll be accessible as "static/myimage.jpg". 8 | You can use this relative route as the source in an Image component. 9 | -------------------------------------------------------------------------------- /apps/hello/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/apps/hello/static/favicon.png -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | plugins = pydantic.mypy 3 | 4 | [mypy-gitignore_parser.*] 5 | ignore_missing_imports = True 6 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | explicit: mark a test to be run only when explicitly specified 4 | set_token: provides a Writer API token mock for the test -------------------------------------------------------------------------------- /src/ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | indent: "off", 4 | "no-unused-vars": "off", 5 | "@typescript-eslint/no-unused-vars": [ 6 | "warn", 7 | { 8 | args: "all", 9 | argsIgnorePattern: "^_", 10 | caughtErrors: "all", 11 | caughtErrorsIgnorePattern: "^_", 12 | destructuredArrayIgnorePattern: "^_", 13 | varsIgnorePattern: "^_", 14 | ignoreRestSiblings: true, 15 | }, 16 | ], 17 | "@typescript-eslint/no-explicit-any": "warn", 18 | "@typescript-eslint/ban-types": "warn", 19 | "prettier/prettier": [2, { useTabs: true, endOfLine: "auto" }], 20 | "no-console": "error", 21 | }, 22 | parser: "vue-eslint-parser", 23 | parserOptions: { 24 | parser: "@typescript-eslint/parser", 25 | ecmaVersion: 2020, 26 | sourceType: "module", 27 | }, 28 | root: true, 29 | extends: [ 30 | "plugin:@typescript-eslint/recommended", 31 | "plugin:vue/vue3-recommended", 32 | "eslint:recommended", 33 | "@vue/eslint-config-prettier", 34 | "prettier", 35 | "plugin:storybook/recommended", 36 | ], 37 | plugins: ["prettier", "@typescript-eslint", "vue"], 38 | env: { 39 | node: true, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | storybook-static/ 7 | src/stories/core_components/ 8 | 9 | *storybook.log 10 | /custom_components_dist 11 | -------------------------------------------------------------------------------- /src/ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /src/ui/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/vue3-vite"; 2 | 3 | import { join, dirname } from "path"; 4 | 5 | /** 6 | * This function is used to resolve the absolute path of a package. 7 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 8 | */ 9 | function getAbsolutePath(value: string): any { 10 | return dirname(require.resolve(join(value, "package.json"))); 11 | } 12 | const config: StorybookConfig = { 13 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 14 | addons: [ 15 | getAbsolutePath("@storybook/addon-links"), 16 | getAbsolutePath("@storybook/addon-essentials"), 17 | getAbsolutePath("@chromatic-com/storybook"), 18 | getAbsolutePath("@storybook/addon-interactions"), 19 | ], 20 | framework: { 21 | name: getAbsolutePath("@storybook/vue3-vite"), 22 | options: {}, 23 | }, 24 | docs: { 25 | autodocs: "tag", 26 | }, 27 | }; 28 | export default config; 29 | -------------------------------------------------------------------------------- /src/ui/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/vue3"; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/i, 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default preview; 15 | -------------------------------------------------------------------------------- /src/ui/public/components/annotatedtext.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/avatar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_addtostatelist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_category_Logic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_category_Triggers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_category_Writer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_foreach.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_logmessage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_parsejson.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_returnvalue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_setstate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_uieventtrigger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_writeraddchatmessage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_writerchat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_writercompletion.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/ui/public/components/blueprints_writerinitchat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ui/public/components/button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/category_Content.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/category_Embed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/category_Input.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/ui/public/components/category_Layout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/ui/public/components/checkboxinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/colorinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ui/public/components/column.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/columns.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/dataframe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/dateinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/dropdowninput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/ui/public/components/fileinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/googlemaps.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/heading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/ui/public/components/horizontalstack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/html.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/ui/public/components/iframe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/jsonviewer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/mapbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/message.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/metric.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/multiselectinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/numberinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/pagination.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/pdf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/plotlygraph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/ui/public/components/progressbar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/ui/public/components/radioinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/rangeinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/ratinginput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/repeater.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/reuse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/section.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/selectinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/ui/public/components/separator.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/sidebar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/sliderinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/step.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/switchinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/tabs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/tags.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/textareainput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/textinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/timeinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/timer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/vegalitechart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/ui/public/components/videoplayer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/components/webcamcapture.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/src/ui/public/favicon.png -------------------------------------------------------------------------------- /src/ui/public/status/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/ui/public/status/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/src/ambientTypes.ts: -------------------------------------------------------------------------------- 1 | import { WriterComponentDefinition } from "./writerTypes"; 2 | 3 | declare module "marked"; 4 | declare module "vue" { 5 | interface ComponentCustomOptions { 6 | writer?: WriterComponentDefinition; 7 | } 8 | } 9 | 10 | export {}; 11 | -------------------------------------------------------------------------------- /src/ui/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/ui/src/assets/note-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ui/src/assets/padding-4-side.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/ui/src/assets/padding-bottom-side.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/src/assets/padding-left-side.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/src/assets/padding-right-side.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/src/assets/padding-top-side.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ui/src/assets/padding-x-side.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/ui/src/assets/padding-y-side.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/ui/src/builder/BuilderAsyncLoader.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /src/ui/src/builder/BuilderCopyText.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/ui/src/builder/BuilderDropFileZone.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | 16 | 41 | -------------------------------------------------------------------------------- /src/ui/src/builder/BuilderInsertionLabel.vue: -------------------------------------------------------------------------------- 1 | 6 | 23 | -------------------------------------------------------------------------------- /src/ui/src/builder/BuilderStateExplorer.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /src/ui/src/builder/BuilderToasts.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | 25 | 47 | -------------------------------------------------------------------------------- /src/ui/src/builder/builderEditorWorker.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor'; 2 | import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; 3 | import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; 4 | import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; 5 | import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; 6 | import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; 7 | 8 | // @ts-ignore 9 | self.MonacoEnvironment = { 10 | getWorker(_: any, label: string) { 11 | if (label === 'json') { 12 | return new jsonWorker(); 13 | } 14 | if (label === 'css' || label === 'scss' || label === 'less') { 15 | return new cssWorker(); 16 | } 17 | if (label === 'html' || label === 'handlebars' || label === 'razor') { 18 | return new htmlWorker(); 19 | } 20 | if (label === 'typescript' || label === 'javascript') { 21 | return new tsWorker(); 22 | } 23 | return new editorWorker(); 24 | } 25 | }; 26 | 27 | monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); 28 | -------------------------------------------------------------------------------- /src/ui/src/builder/ico.css: -------------------------------------------------------------------------------- 1 | .ico { 2 | background-size: 12px 12px; 3 | width: 12px; 4 | height: 12px; 5 | display: block; 6 | } 7 | 8 | .ico-padding-4-side { 9 | background: url("../assets/padding-4-side.svg"); 10 | } 11 | 12 | .ico-padding-x-side { 13 | background: url("../assets/padding-x-side.svg"); 14 | } 15 | 16 | .ico-padding-y-side { 17 | background: url("../assets/padding-y-side.svg"); 18 | } 19 | 20 | .ico-padding-left-side { 21 | background: url("../assets/padding-left-side.svg"); 22 | } 23 | 24 | .ico-padding-right-side { 25 | background: url("../assets/padding-right-side.svg"); 26 | } 27 | 28 | .ico-padding-top-side { 29 | background: url("../assets/padding-top-side.svg"); 30 | } 31 | 32 | .ico-padding-bottom-side { 33 | background: url("../assets/padding-bottom-side.svg"); 34 | } -------------------------------------------------------------------------------- /src/ui/src/builder/panels/BuilderCodePanelFileUploadBtn.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | -------------------------------------------------------------------------------- /src/ui/src/builder/panels/BuilderCodePanelFileUploading.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /src/ui/src/builder/settings/BuilderSectionTitle.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /src/ui/src/builder/settings/constants/builderFieldsCssTabs.ts: -------------------------------------------------------------------------------- 1 | import type { WdsTabOptions } from "@/wds/WdsTabs.vue"; 2 | 3 | export type BuilderFieldCssMode = "pick" | "css" | "default"; 4 | 5 | export const BUILDER_FIELD_CSS_TAB_OPTIONS = Object.freeze< 6 | WdsTabOptions[] 7 | >([ 8 | { 9 | label: "Default", 10 | value: "default", 11 | }, 12 | { 13 | label: "CSS", 14 | value: "css", 15 | }, 16 | { 17 | label: "Pick", 18 | value: "pick", 19 | }, 20 | ]); 21 | -------------------------------------------------------------------------------- /src/ui/src/builder/useComponentClipboard.ts: -------------------------------------------------------------------------------- 1 | import { useLogger } from "@/composables/useLogger"; 2 | import { Component } from "@/writerTypes"; 3 | 4 | type ClipboardData = { 5 | type: "writer/clipboard"; 6 | components: Component[]; 7 | }; 8 | 9 | function isClipboardData(data: unknown): data is ClipboardData { 10 | return ( 11 | typeof data === "object" && 12 | data !== null && 13 | "type" in data && 14 | data.type === "writer/clipboard" && 15 | "components" in data && 16 | Array.isArray(data.components) 17 | ); 18 | } 19 | 20 | export function useComponentClipboard() { 21 | const logger = useLogger(); 22 | 23 | async function get(): Promise { 24 | try { 25 | const text = await navigator.clipboard.readText(); 26 | const data = JSON.parse(text); 27 | return isClipboardData(data) ? data.components : undefined; 28 | } catch { 29 | return undefined; 30 | } 31 | } 32 | 33 | function set(value: Component[]) { 34 | const data: ClipboardData = { 35 | type: "writer/clipboard", 36 | components: value, 37 | }; 38 | try { 39 | navigator.clipboard.writeText(JSON.stringify(data)); 40 | } catch (e) { 41 | logger.warn("Failed to write components in the clipboard", e); 42 | } 43 | } 44 | 45 | return { get, set }; 46 | } 47 | -------------------------------------------------------------------------------- /src/ui/src/builder/useToast.ts: -------------------------------------------------------------------------------- 1 | import { readonly, shallowRef } from "vue"; 2 | 3 | export type ToastAction = { label: string; func: () => void; icon: string }; 4 | 5 | export type Toast = { 6 | id: number; 7 | type: "error" | "success" | "info"; 8 | message: string; 9 | closable?: boolean; 10 | delayMs?: number; 11 | action?: ToastAction; 12 | }; 13 | 14 | const toasts = shallowRef([]); 15 | 16 | export function useToasts() { 17 | function removeToast(id: number) { 18 | toasts.value = toasts.value.filter((t) => t.id !== id); 19 | } 20 | 21 | function pushToast(toast: Omit) { 22 | const id = new Date().getTime(); 23 | toasts.value = [...toasts.value, { ...toast, id }]; 24 | 25 | if (!toast.closable) { 26 | setTimeout(() => removeToast(id), toast.delayMs ?? 3_000); 27 | } 28 | } 29 | 30 | return { 31 | pushToast, 32 | removeToast, 33 | toasts: readonly(toasts), 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/src/components/core/base/BaseContainer.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 42 | 43 | 51 | -------------------------------------------------------------------------------- /src/ui/src/components/core/base/BaseInputSlider.utils.ts: -------------------------------------------------------------------------------- 1 | import { computed, Ref } from "vue"; 2 | 3 | /** 4 | * Format a number using `toFixed` according to the number of floating number in the `step` 5 | */ 6 | export function useNumberFormatByStep( 7 | value: Ref, 8 | step: Ref, 9 | ) { 10 | const precision = computed( 11 | () => String(step.value).split(".")[1]?.length ?? 0, 12 | ); 13 | return computed(() => Number(value.value).toFixed(precision.value)); 14 | } 15 | -------------------------------------------------------------------------------- /src/ui/src/components/core/base/BaseInputWrapper.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | 19 | 47 | -------------------------------------------------------------------------------- /src/ui/src/components/core/base/BaseMarkdown.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | -------------------------------------------------------------------------------- /src/ui/src/components/core/base/BaseTransitionSlideFade.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 28 | -------------------------------------------------------------------------------- /src/ui/src/components/core/content/CoreChatBot/CoreChatbotAvatar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 29 | -------------------------------------------------------------------------------- /src/ui/src/components/core/content/CoreChatBot/CoreChatbotSentMessageIcon.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /src/ui/src/components/core/content/CoreDataframe/CoreDataframeCellUnknown.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 37 | -------------------------------------------------------------------------------- /src/ui/src/components/core/content/CoreDataframe/useJobs.ts: -------------------------------------------------------------------------------- 1 | import { useLogger } from "@/composables/useLogger"; 2 | import { shallowRef, ref, readonly } from "vue"; 3 | 4 | /** 5 | * A simple FIFO Job queue algorithm 6 | */ 7 | export function useJobs(handler: (value: T) => Promise) { 8 | const jobs = shallowRef([]); 9 | const isRunning = ref(false); 10 | const logger = useLogger(); 11 | 12 | async function run() { 13 | if (isRunning.value) return; 14 | 15 | isRunning.value = true; 16 | 17 | while (jobs.value.length) { 18 | const [job, ...rest] = jobs.value; 19 | 20 | try { 21 | await handler(job); 22 | } catch (error) { 23 | logger.error("Error during handling job", job, error); 24 | } finally { 25 | jobs.value = rest; 26 | } 27 | } 28 | 29 | isRunning.value = false; 30 | } 31 | 32 | function push(job: T) { 33 | jobs.value = [...jobs.value, job]; 34 | if (!isRunning.value) return run(); 35 | } 36 | 37 | return { push, isBusy: readonly(isRunning) }; 38 | } 39 | -------------------------------------------------------------------------------- /src/ui/src/components/core/content/__snapshots__/CoreText.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`CoreText > should render in markdown mode 1`] = ` 4 |
9 | 10 | 17 | 18 |
19 | `; 20 | 21 | exports[`CoreText > should render in non-markdown mode 1`] = ` 22 |
27 | 28 |

33 | # Hello 34 | 35 | I'm the **content**. 36 |

37 | 38 |
39 | `; 40 | -------------------------------------------------------------------------------- /src/ui/src/components/core/input/__snapshots__/CoreCheckboxInput.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`CoreCheckboxInput > should render value from the state and forward emit 1`] = ` 4 |
9 | 14 | 15 |
19 | 20 |
24 | 29 | 35 |
36 |
40 | 46 | 52 |
53 | 54 |
55 | 56 |
57 | `; 58 | -------------------------------------------------------------------------------- /src/ui/src/components/core/input/__snapshots__/CoreDateInput.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`CoreDateInput > should render value from the state and forward emit 1`] = ` 4 |
9 | 14 | 15 | 20 | 21 |
22 | `; 23 | -------------------------------------------------------------------------------- /src/ui/src/components/core/internal/CoreNote.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 43 | -------------------------------------------------------------------------------- /src/ui/src/components/core/layout/CoreColumns.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 30 | 31 | 41 | -------------------------------------------------------------------------------- /src/ui/src/components/core/layout/CoreSeparator.vue: -------------------------------------------------------------------------------- 1 | 2 | _Separator_ components are used to separate layout elements. They can be used in most containers, including _Column Container_ to separate columns. 3 | 4 | If the container flows horizontally (like a _Horizontal Stack_ or a _Column Container_) the _Separator_ will be a vertical line. Otherwise, it'll be a horizontal line. 5 | 6 | 7 | 12 | 13 | 31 | 32 | 50 | -------------------------------------------------------------------------------- /src/ui/src/components/core/other/CoreHtml.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import CoreHtml from "./CoreHtml.vue"; 4 | import { flushPromises, mount } from "@vue/test-utils"; 5 | import injectionKeys from "@/injectionKeys"; 6 | import { ref } from "vue"; 7 | import { mockProvides } from "@/tests/mocks"; 8 | 9 | describe("CoreHtml", () => { 10 | it("should filter invalid Attributes props", async () => { 11 | const wrapper = mount(CoreHtml, { 12 | slots: { 13 | default: "slot", 14 | }, 15 | global: { 16 | provide: { 17 | ...mockProvides, 18 | [injectionKeys.evaluatedFields as symbol]: { 19 | htmlInside: ref("inside"), 20 | element: ref("div"), 21 | styles: ref({ 22 | color: "red", 23 | }), 24 | attrs: ref({ 25 | "0invalid": "invalid", 26 | valid: "valid", 27 | }), 28 | }, 29 | }, 30 | }, 31 | }); 32 | 33 | await flushPromises(); 34 | 35 | const attrs = wrapper.attributes(); 36 | 37 | expect(attrs.valid).toBe("valid"); 38 | expect(attrs.invalid).toBeUndefined(); 39 | expect(attrs.style).toBe("color: red;"); 40 | 41 | expect(wrapper.element).toMatchSnapshot(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/ui/src/components/core/other/__snapshots__/CoreHtml.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`CoreHtml > should filter invalid Attributes props 1`] = ` 4 |
10 | 11 | slot 12 | 13 |
14 | inside 15 |
16 |
17 | `; 18 | -------------------------------------------------------------------------------- /src/ui/src/components/custom/index.ts: -------------------------------------------------------------------------------- 1 | // Import the templates 2 | 3 | import type { TemplateMap } from "@/writerTypes"; 4 | import BubbleMessage from "./BubbleMessage.vue"; 5 | import BubbleMessageAdvanced from "./BubbleMessageAdvanced.vue"; 6 | 7 | // Export an object with the ids and the templates as default 8 | 9 | const CUSTOM_COMPONENTS: TemplateMap = { 10 | bubblemessage: BubbleMessage, 11 | bubblemessageadvanced: BubbleMessageAdvanced, 12 | }; 13 | 14 | export default CUSTOM_COMPONENTS; 15 | -------------------------------------------------------------------------------- /src/ui/src/components/shared/README.md: -------------------------------------------------------------------------------- 1 | # Shared components 2 | 3 | This folder contains Vue.js components that are used in Builder **and** in Renderer side. 4 | -------------------------------------------------------------------------------- /src/ui/src/components/shared/SharedControlBar.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 28 | 29 | 38 | -------------------------------------------------------------------------------- /src/ui/src/components/shared/SharedImgWithFallback.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach, Mock } from "vitest"; 2 | import SharedImgWithFallback from "./SharedImgWithFallback.vue"; 3 | import { flushPromises, shallowMount } from "@vue/test-utils"; 4 | 5 | describe("SharedImgWithFallback", () => { 6 | let fetch: Mock; 7 | 8 | beforeEach(() => { 9 | fetch = vi.fn().mockResolvedValue({ 10 | ok: true, 11 | headers: new Map([["Content-Type", "image/png"]]), 12 | }); 13 | global.fetch = fetch; 14 | }); 15 | 16 | it("should use the last image because the first two are not valid", async () => { 17 | fetch 18 | .mockRejectedValueOnce(new Error()) 19 | .mockResolvedValueOnce({ 20 | ok: true, 21 | headers: new Map([["Content-Type", "text/html"]]), 22 | }) 23 | .mockResolvedValue({ 24 | ok: true, 25 | headers: new Map([["Content-Type", "image/png"]]), 26 | }); 27 | 28 | const wrapper = shallowMount(SharedImgWithFallback, { 29 | props: { urls: ["/img1.svg", "/img2.svg", "/img3.svg"] }, 30 | }); 31 | expect(wrapper.get("img").attributes().src).toBe(""); 32 | 33 | await flushPromises(); 34 | 35 | expect(wrapper.get("img").attributes().src).toBe("/img3.svg"); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/ui/src/components/shared/SharedImgWithFallback.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /src/ui/src/components/shared/SharedJsonViewer/SharedJsonViewer.utils.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | JsonData, 3 | JsonValue, 4 | JsonViewerTogglePayload, 5 | } from "./SharedJsonViewer.vue"; 6 | 7 | export function isJSONValue(data: JsonData): data is JsonValue { 8 | if (["string", "number", "boolean"].includes(typeof data)) return true; 9 | if (data === null) return true; 10 | return false; 11 | } 12 | 13 | export function isJSONArray(data: JsonData): data is JsonData[] { 14 | if (isJSONValue(data)) return false; 15 | return Array.isArray(data); 16 | } 17 | 18 | export function isJSONObject( 19 | data: JsonData, 20 | ): data is { [x: string]: JsonData } { 21 | return !isJSONArray(data) && typeof data === "object" && data !== null; 22 | } 23 | 24 | export function getJSONLength(data: JsonData): number { 25 | return isJSONValue(data) ? 1 : Object.keys(data).length; 26 | } 27 | 28 | export function jsonViewerToggleEmitDefinition( 29 | payload: JsonViewerTogglePayload, 30 | ) { 31 | return typeof payload.open === "boolean" && Array.isArray(payload.path); 32 | } 33 | -------------------------------------------------------------------------------- /src/ui/src/components/shared/SharedJsonViewer/SharedJsonViewerChildrenCounter.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 40 | 41 | 48 | -------------------------------------------------------------------------------- /src/ui/src/components/shared/SharedJsonViewer/SharedJsonViewerObject.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import SharedJsonViewerObject from "./SharedJsonViewerObject.vue"; 3 | import { flushPromises, mount } from "@vue/test-utils"; 4 | import SharedJsonViewerCollapsible from "./SharedJsonViewerCollapsible.vue"; 5 | import SharedJsonViewer from "./SharedJsonViewer.vue"; 6 | 7 | describe("SharedJsonViewerObject", () => { 8 | it("should expand a key", async () => { 9 | const data = { array: [1, 2, 3], obj: { a: 1, b: 2, c: 3 } }; 10 | const wrapper = mount(SharedJsonViewerObject, { props: { data } }); 11 | 12 | const collapsers = wrapper.findAllComponents( 13 | SharedJsonViewerCollapsible, 14 | ); 15 | expect(collapsers).toHaveLength(2); 16 | 17 | const arrayCollapser = collapsers.at(0); 18 | 19 | expect(arrayCollapser.props().open).toBeFalsy(); 20 | 21 | arrayCollapser.vm.$emit("toggle", true); 22 | await flushPromises(); 23 | 24 | expect(arrayCollapser.props().open).toBeTruthy(); 25 | 26 | const arrayElement = wrapper.getComponent(SharedJsonViewer); 27 | expect(arrayElement.props().data).toStrictEqual(data.array); 28 | expect(arrayElement.props().path).toStrictEqual(["array"]); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/ui/src/components/shared/SharedJsonViewer/SharedJsonViewerValue.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 25 | 26 | 32 | -------------------------------------------------------------------------------- /src/ui/src/composables/useAbortController.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeUnmount } from "vue"; 2 | 3 | export function useAbortController() { 4 | const abortController = new AbortController(); 5 | 6 | onBeforeUnmount(() => { 7 | abortController.abort(); 8 | }); 9 | 10 | return abortController; 11 | } 12 | -------------------------------------------------------------------------------- /src/ui/src/composables/useAssetContentType.ts: -------------------------------------------------------------------------------- 1 | const cacheUrlContentType = new Map>(); 2 | 3 | /** 4 | * Do an HTTP `HEAD` call to get the `Content-Type` of an URL. Handle parrallel calls and use a cache mechanism. 5 | */ 6 | export function useAssetContentType() { 7 | function fetchAssetContentType(url: string) { 8 | const cachedValue = cacheUrlContentType.get(url); 9 | if (cachedValue !== undefined) return cachedValue; 10 | 11 | // we store the promise instead of the result to handle concurent calls 12 | const promise = fetch(url, { method: "HEAD" }) 13 | .then((r) => { 14 | if (!r.ok) return undefined; 15 | return r.headers.get("Content-Type") || undefined; 16 | }) 17 | .catch(() => undefined); 18 | 19 | cacheUrlContentType.set(url, promise); 20 | 21 | return promise; 22 | } 23 | 24 | function clearCache() { 25 | cacheUrlContentType.clear(); 26 | } 27 | 28 | return { fetchAssetContentType, clearCache }; 29 | } 30 | -------------------------------------------------------------------------------- /src/ui/src/composables/useBoundingClientRect.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref, onUnmounted, computed } from "vue"; 2 | 3 | /** 4 | * Watch the bounding client rect of an element using `setInterval` 5 | */ 6 | export function useBoundingClientRect(htmlRef: Ref, ms = 500) { 7 | const rect = ref(); 8 | 9 | const id = setInterval(() => { 10 | rect.value = htmlRef.value?.getBoundingClientRect(); 11 | }, ms); 12 | 13 | onUnmounted(() => { 14 | clearInterval(id); 15 | }); 16 | 17 | return computed(() => rect.value); 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/src/composables/useComponentBlueprints.ts: -------------------------------------------------------------------------------- 1 | import type { Core, Component } from "@/writerTypes"; 2 | import { computed, ComputedRef, unref } from "vue"; 3 | 4 | export function useComponentLinkedBlueprints( 5 | wf: Core, 6 | componentId: ComputedRef | string, 7 | eventType: ComputedRef | string, 8 | ) { 9 | function isBlueprintTrigger(c: Component) { 10 | return ( 11 | c.type === "blueprints_uieventtrigger" && 12 | c.content.refComponentId === unref(componentId) && 13 | c.content.refEventType === unref(eventType) 14 | ); 15 | } 16 | 17 | const blueprints = computed(() => { 18 | const blueprintIds = new Set(triggers.value.map((c) => c.parentId)); 19 | 20 | return [...blueprintIds] 21 | .map((i) => wf.getComponentById(i)) 22 | .filter(Boolean); 23 | }); 24 | 25 | const triggers = computed(() => { 26 | return wf 27 | .getComponents("blueprints_root") 28 | .flatMap((w) => wf.getComponents(w.id).filter(isBlueprintTrigger)); 29 | }); 30 | 31 | const isLinked = computed(() => triggers.value.length > 0); 32 | 33 | return { blueprints, triggers, isLinked }; 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/src/composables/useDebouncer.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, vi, expect, beforeAll, afterAll } from "vitest"; 2 | import { useDebouncer } from "./useDebouncer"; 3 | 4 | describe(useDebouncer.name, () => { 5 | beforeAll(() => { 6 | vi.useFakeTimers(); 7 | }); 8 | 9 | afterAll(() => { 10 | vi.useRealTimers(); 11 | }); 12 | 13 | it("should call the callback one time", () => { 14 | const callback = vi.fn(); 15 | 16 | const callbackDebounced = useDebouncer(callback, 1_000); 17 | 18 | callbackDebounced(); 19 | callbackDebounced(); 20 | vi.advanceTimersByTime(1_000); 21 | expect(callback).toHaveBeenCalledTimes(1); 22 | 23 | callbackDebounced(); 24 | vi.advanceTimersByTime(500); 25 | expect(callback).toHaveBeenCalledTimes(1); 26 | 27 | callbackDebounced(); 28 | vi.advanceTimersByTime(1_000); 29 | expect(callback).toHaveBeenCalledTimes(2); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/ui/src/composables/useDebouncer.ts: -------------------------------------------------------------------------------- 1 | export function useDebouncer(callback: () => void | Promise, ms: number) { 2 | let id: ReturnType; 3 | return () => { 4 | if (id) clearTimeout(id); 5 | id = setTimeout(callback, ms); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/ui/src/composables/useFormatter.ts: -------------------------------------------------------------------------------- 1 | import { computed, ComputedRef } from "vue"; 2 | 3 | export function usePercentageFormatter( 4 | number: ComputedRef, 5 | options: Pick< 6 | Intl.NumberFormatOptions, 7 | "minimumFractionDigits" | "maximumFractionDigits" 8 | > = { 9 | minimumFractionDigits: 0, 10 | maximumFractionDigits: 1, 11 | }, 12 | ) { 13 | const formatter = new Intl.NumberFormat(undefined, { 14 | style: "percent", 15 | ...options, 16 | }); 17 | return computed(() => formatter.format(number.value)); 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/src/composables/useListResources.ts: -------------------------------------------------------------------------------- 1 | import { generateCore } from "@/core"; 2 | import { readonly, ref, shallowRef } from "vue"; 3 | import { useLocalStorageJSON } from "./useLocalStorageJSON"; 4 | 5 | export function useListResources( 6 | wf: ReturnType, 7 | type: string, 8 | ) { 9 | const isLoading = ref(false); 10 | const error = ref(); 11 | 12 | const cache = useLocalStorageJSON( 13 | `useListResources(${type})`, 14 | Array.isArray, 15 | ); 16 | 17 | const data = shallowRef(cache.value ?? []); 18 | 19 | async function load() { 20 | isLoading.value = true; 21 | error.value = undefined; 22 | try { 23 | data.value = await wf.sendListResourcesRequest(type); 24 | cache.value = data.value; 25 | } catch (e) { 26 | error.value = e; 27 | data.value = []; 28 | } finally { 29 | isLoading.value = false; 30 | } 31 | } 32 | 33 | return { 34 | data: readonly(data), 35 | isLoading: readonly(isLoading), 36 | error: readonly(error), 37 | load, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/ui/src/composables/useLocalStorageJSON.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "vue"; 2 | 3 | /** 4 | * Get/Set the JSON object in localStorage 5 | * @param validator validate that the data has a given shape, remote the localStorage value if returns `false` 6 | */ 7 | export function useLocalStorageJSON( 8 | key: string, 9 | validator?: (value: T) => boolean, 10 | ) { 11 | return computed({ 12 | get() { 13 | const value = localStorage.getItem(key); 14 | if (!value) return undefined; 15 | 16 | try { 17 | const data = JSON.parse(value); 18 | if (validator?.(data) === false) { 19 | localStorage.removeItem(key); 20 | return undefined; 21 | } 22 | return data; 23 | } catch { 24 | localStorage.removeItem(key); 25 | return undefined; 26 | } 27 | }, 28 | set(value) { 29 | value === undefined 30 | ? localStorage.removeItem(key) 31 | : localStorage.setItem(key, JSON.stringify(value)); 32 | }, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/src/composables/useLogger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | /** 4 | * A simple abstraction to use logger in the application. For the moment, it's just a proxy to `console`, but it can be plugged to any library later. 5 | */ 6 | export function useLogger(): Pick< 7 | typeof console, 8 | "log" | "warn" | "info" | "error" 9 | > { 10 | return { 11 | log: console.log, 12 | warn: console.warn, 13 | info: console.info, 14 | error: console.error, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/src/composables/useWriterApi.ts: -------------------------------------------------------------------------------- 1 | import { WriterApi } from "@/writerApi"; 2 | 3 | export function useWriterApi(opts: { signal?: AbortSignal } = {}) { 4 | const apiBaseUrl = 5 | // @ts-expect-error use injected variable from Vite to specify the host on local env 6 | import.meta.env.VITE_WRITER_BASE_URL ?? window.location.origin; 7 | 8 | const writerApi = new WriterApi({ 9 | ...opts, 10 | baseUrl: apiBaseUrl, 11 | }); 12 | 13 | return { writerApi, apiBaseUrl }; 14 | } 15 | -------------------------------------------------------------------------------- /src/ui/src/constants/component.ts: -------------------------------------------------------------------------------- 1 | export const COMPONENT_TYPES_ROOT = new Set(["root", "blueprints_root"]); 2 | 3 | export const COMPONENT_TYPES_TOP_LEVEL = new Set([ 4 | ...COMPONENT_TYPES_ROOT, 5 | "page", 6 | "blueprints_blueprint", 7 | ]); 8 | -------------------------------------------------------------------------------- /src/ui/src/constants/icons.ts: -------------------------------------------------------------------------------- 1 | export const BUILDER_MANAGER_MODE_ICONS = Object.freeze< 2 | Record<"ui" | "blueprints" | "preview", string> 3 | >({ 4 | ui: "grid_3x3", 5 | blueprints: "linked_services", 6 | preview: "visibility", 7 | }); 8 | -------------------------------------------------------------------------------- /src/ui/src/core/detectPlatform.ts: -------------------------------------------------------------------------------- 1 | function getPlatform() { 2 | const platform: string = 3 | navigator?.userAgentData?.platform || navigator?.platform; 4 | return platform; 5 | } 6 | 7 | export function getModifierKeyName() { 8 | return isPlatformMac() ? "⌘ " : "Ctrl+"; 9 | } 10 | 11 | export function isModifierKeyActive(ev: KeyboardEvent | MouseEvent) { 12 | return isPlatformMac() ? ev.metaKey : ev.ctrlKey; 13 | } 14 | 15 | export function isPlatformMac() { 16 | const platform = getPlatform(); 17 | if (platform.toLowerCase().indexOf("mac") != -1) return true; 18 | return false; 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/src/core/parsing.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Splits a flat state accessor e.g. "animals.dog" into an array ["animals", "dog"] 3 | * Dots can be escaped with backslash. 4 | * 5 | * @param s 6 | */ 7 | export function parseAccessor(s: string): string[] { 8 | let currentItem = ""; 9 | const accessor = []; 10 | let isEscaped = false; 11 | 12 | // Avoided regex for speed and because Safari doesn't support negative lookbehind 13 | 14 | for (let i = 0; i < s.length; i++) { 15 | const currentChar = s[i]; 16 | if (currentChar === "." && !isEscaped) { 17 | accessor.push(currentItem); 18 | currentItem = ""; 19 | continue; 20 | } 21 | currentItem += currentChar; 22 | if (currentChar === "\\") { 23 | isEscaped = true; 24 | } else { 25 | isEscaped = false; 26 | } 27 | } 28 | accessor.push(currentItem); 29 | 30 | const replacedAccessor = accessor.map((s) => s.replaceAll("\\.", ".")); 31 | 32 | return replacedAccessor; 33 | } 34 | -------------------------------------------------------------------------------- /src/ui/src/core/serializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * JSON serializer to handle bigint 3 | */ 4 | export function bigIntReplacer(_key: string, value: unknown): unknown { 5 | if (typeof value === "bigint") { 6 | return value.toString() + "n"; 7 | } 8 | return value; 9 | } 10 | -------------------------------------------------------------------------------- /src/ui/src/directives.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | 3 | export function setCaptureTabsDirective(app: App) { 4 | app.directive("capture-tabs", { 5 | mounted: (el: HTMLTextAreaElement) => { 6 | el.addEventListener("keydown", (ev) => { 7 | if (ev.key != "Tab") return; 8 | ev.preventDefault(); 9 | el.setRangeText( 10 | " ", 11 | el.selectionStart, 12 | el.selectionStart, 13 | "end", 14 | ); 15 | }); 16 | }, 17 | }); 18 | } -------------------------------------------------------------------------------- /src/ui/src/fonts.ts: -------------------------------------------------------------------------------- 1 | import "@fontsource-variable/material-symbols-outlined"; 2 | 3 | import "@fontsource/poppins/300-italic.css"; 4 | import "@fontsource/poppins/300.css"; 5 | import "@fontsource/poppins/400-italic.css"; 6 | import "@fontsource/poppins/400.css"; 7 | import "@fontsource/poppins/500-italic.css"; 8 | import "@fontsource/poppins/500.css"; 9 | import "@fontsource/poppins/600-italic.css"; 10 | import "@fontsource/poppins/600.css"; 11 | import "@fontsource/poppins/700-italic.css"; 12 | import "@fontsource/poppins/700.css"; 13 | -------------------------------------------------------------------------------- /src/ui/src/injectionKeys.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, InjectionKey, Ref, VNode } from "vue"; 2 | import { 3 | BuilderManager, 4 | CollaborationManager, 5 | Component, 6 | Core, 7 | InstancePath, 8 | InstancePathItem, 9 | NotesManager, 10 | } from "./writerTypes"; 11 | 12 | export default { 13 | core: Symbol() as InjectionKey, 14 | builderManager: Symbol() as InjectionKey, 15 | notesManager: Symbol() as InjectionKey, 16 | collaborationManager: Symbol() as InjectionKey, 17 | evaluatedFields: Symbol() as InjectionKey>>, 18 | componentId: Symbol() as InjectionKey, 19 | isBeingEdited: Symbol() as InjectionKey>, 20 | isDisabled: Symbol() as InjectionKey>, 21 | getChildrenVNodes: Symbol() as InjectionKey< 22 | (instanceNumber?: InstancePathItem["instanceNumber"]) => VNode[] 23 | >, 24 | renderProxiedComponent: Symbol() as InjectionKey< 25 | ( 26 | componentId: Component["id"], 27 | instanceNumber: InstancePathItem["instanceNumber"], 28 | ext?: { class?: string[]; contextSlot?: string }, 29 | ) => VNode 30 | >, 31 | instancePath: Symbol() as InjectionKey, 32 | flattenedInstancePath: Symbol() as InjectionKey, 33 | instanceData: Symbol() as InjectionKey, 34 | }; 35 | -------------------------------------------------------------------------------- /src/ui/src/renderer/LoadingSymbol.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 31 | -------------------------------------------------------------------------------- /src/ui/src/renderer/RenderError.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/ui/src/renderer/colorTransformations.css: -------------------------------------------------------------------------------- 1 | .component, .colorTransformer { 2 | --softenedAccentColor: var(--accentColor); 3 | --intensifiedAccentColor: var(--accentColor); 4 | --intensifiedButtonColor: var(--buttonColor); 5 | --softenedSeparatorColor: var(--separatorColor); 6 | --intensifiedSeparatorColor: var(--separatorColor); 7 | } 8 | 9 | @supports (color: hsl(from red h s calc(l - 20))) { 10 | .component, .colorTransformer { 11 | --softenedAccentColor: hsl( 12 | from var(--accentColor) calc(h - 12) calc(s + 0) calc(l + 0.21) 13 | ); 14 | --intensifiedAccentColor: hsl( 15 | from var(--accentColor) calc(h + 1) calc(s - 0.33) calc(l - 0.1) 16 | ); 17 | --intensifiedButtonColor: hsl( 18 | from var(--buttonColor) calc(h + 1) calc(s - 33) calc(l - 10) 19 | ); 20 | --softenedSeparatorColor: hsl( 21 | from var(--separatorColor) calc(h - 0) calc(s + 0.05) calc(l + 0.06) 22 | ); 23 | --intensifiedSeparatorColor: hsl( 24 | from var(--separatorColor) calc(h - 0) calc(s - 0.06) calc(l - 0.52) 25 | ); 26 | } 27 | } -------------------------------------------------------------------------------- /src/ui/src/renderer/instancePath.ts: -------------------------------------------------------------------------------- 1 | import type { InstancePath } from "@/writerTypes"; 2 | 3 | export function flattenInstancePath(path: InstancePath) { 4 | return path.map((ie) => `${ie.componentId}:${ie.instanceNumber}`).join(","); 5 | } 6 | 7 | export function parseInstancePathString(raw?: string): InstancePath { 8 | if (!raw) return []; 9 | return raw.split(",").map((record) => { 10 | const [componentId, instanceNumber] = record.split(":"); 11 | return { componentId, instanceNumber: Number(instanceNumber) }; 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/ui/src/renderer/syntheticEvents.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param ev event from a mouse click, or a keyboard click (using tab navigation, then click with `Enter`) 3 | */ 4 | export function getClick(ev: MouseEvent | KeyboardEvent): CustomEvent { 5 | const payload = { 6 | ctrlKey: ev.ctrlKey, 7 | shiftKey: ev.shiftKey, 8 | metaKey: ev.metaKey, 9 | }; 10 | const event = new CustomEvent("wf-click", { 11 | detail: { 12 | payload, 13 | }, 14 | }); 15 | return event; 16 | } 17 | 18 | export function getKeydown(ev: KeyboardEvent): CustomEvent { 19 | const payload = { 20 | key: ev.key, 21 | ctrlKey: ev.ctrlKey, 22 | shiftKey: ev.shiftKey, 23 | metaKey: ev.metaKey, 24 | }; 25 | const event = new CustomEvent("wf-keydown", { 26 | detail: { 27 | payload, 28 | }, 29 | }); 30 | return event; 31 | } 32 | -------------------------------------------------------------------------------- /src/ui/src/stories/fakeCore.ts: -------------------------------------------------------------------------------- 1 | import { getComponentDefinition } from "../core/templateMap"; 2 | 3 | export const generateCore = () => { 4 | return { 5 | getComponentDefinition, 6 | getComponentById: (id) => ({id}), 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/ui/src/utils/base64.ts: -------------------------------------------------------------------------------- 1 | export function dataUrlToBase64(url: string) { 2 | const base64 = url.split(",")?.[1]; 3 | if (!base64) throw Error(`Could not extract base64 from data url: ${url}`); 4 | return base64; 5 | } 6 | 7 | export function base64ToArrayBuffer(base64: string) { 8 | const binaryString = window.atob(base64); 9 | const len = binaryString.length; 10 | const bytes = new Uint8Array(len); 11 | for (let i = 0; i < len; i++) { 12 | bytes[i] = binaryString.charCodeAt(i); 13 | } 14 | return bytes.buffer; 15 | } 16 | 17 | export function dataURLToArrayBuffer(dataURL: string) { 18 | const base64String = dataUrlToBase64(dataURL); 19 | const binaryString = atob(base64String); 20 | 21 | const buffer = new ArrayBuffer(binaryString.length); 22 | const bytes = new Uint8Array(buffer); 23 | 24 | for (let i = 0; i < binaryString.length; i++) { 25 | bytes[i] = binaryString.charCodeAt(i); 26 | } 27 | 28 | return buffer; 29 | } 30 | -------------------------------------------------------------------------------- /src/ui/src/utils/math.ts: -------------------------------------------------------------------------------- 1 | export function mathCeilToMultiple(nb: number, multiple: number) { 2 | return Math.ceil(nb / multiple) * multiple; 3 | } 4 | export function mathRoundToMultiple(nb: number, multiple: number) { 5 | return Math.round(nb / multiple) * multiple; 6 | } 7 | -------------------------------------------------------------------------------- /src/ui/src/utils/url.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { convertAbsolutePathtoFullURL } from "./url"; 3 | 4 | describe(convertAbsolutePathtoFullURL.name, () => { 5 | it("should convert the URL", () => { 6 | expect( 7 | convertAbsolutePathtoFullURL( 8 | "/assets/image.png", 9 | "http://localhost:3000/", 10 | ), 11 | ).toBe("http://localhost:3000/assets/image.png"); 12 | }); 13 | 14 | it("should convert the URL with a current path", () => { 15 | expect( 16 | convertAbsolutePathtoFullURL( 17 | "/assets/image.png", 18 | "http://localhost:3000/hello/?foo=bar", 19 | ), 20 | ).toBe("http://localhost:3000/hello/assets/image.png"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/ui/src/utils/url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert absoule URL to full URL in case the application is hosted on a subpath. 3 | * 4 | * ```js 5 | * convertAbsolutePathtoFullURL("/assets/image.png", "http://localhost:3000/hello/?foo=bar") 6 | * // => 'http://localhost:3000/hello/assets/image.png' 7 | * ``` 8 | */ 9 | export function convertAbsolutePathtoFullURL( 10 | path: string, 11 | base = window.location.toString(), 12 | ) { 13 | return new URL(`.${path}`, base).toString(); 14 | } 15 | -------------------------------------------------------------------------------- /src/ui/src/wds/WdsControl.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 25 | -------------------------------------------------------------------------------- /src/ui/src/wds/WdsDropdownInput.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 30 | -------------------------------------------------------------------------------- /src/ui/src/wds/WdsLoaderDots.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 46 | -------------------------------------------------------------------------------- /src/ui/src/wds/WdsSkeletonLoader.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 32 | -------------------------------------------------------------------------------- /src/ui/src/wds/WdsTab.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | 31 | 56 | -------------------------------------------------------------------------------- /src/ui/src/wds/WdsTabs.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 21 | 22 | 35 | 36 | 43 | -------------------------------------------------------------------------------- /src/ui/tools/core.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import path from "path"; 3 | 4 | import { createServer } from "vite"; 5 | import { fileURLToPath } from "url"; 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | /** 10 | * Loads the definition of Writer Framework components. 11 | * 12 | * @returns {Promise} The components. 13 | */ 14 | export async function loadComponents() { 15 | const vite = await createServer({ 16 | includeWriterComponentPath: true, 17 | server: { 18 | middlewareMode: true, 19 | }, 20 | appType: "custom", 21 | }); 22 | 23 | const { data } = await vite.ssrLoadModule( 24 | path.join(__dirname, "getComponents.ts"), 25 | ); 26 | 27 | await vite.close(); 28 | 29 | return data; 30 | } 31 | 32 | /** 33 | * imports a vue-dependent module. 34 | */ 35 | export async function importVue(modulePath) { 36 | const vite = await createServer({ 37 | includeWriterComponentPath: true, 38 | server: { 39 | middlewareMode: true, 40 | }, 41 | appType: "custom", 42 | }); 43 | 44 | const m = await vite.ssrLoadModule(path.join(__dirname, modulePath)); 45 | await vite.close(); 46 | 47 | return m; 48 | } 49 | -------------------------------------------------------------------------------- /src/ui/tools/custom_check.mjs: -------------------------------------------------------------------------------- 1 | import { importVue } from "./core.mjs"; 2 | 3 | async function checkDeclarationKey() { 4 | let hasFailed = false; 5 | const module = await importVue("../src/components/custom/index.ts"); 6 | const { checkComponentKey } = await importVue( 7 | "../src/core/loadExtensions.ts", 8 | ); 9 | const invalidCustomComponentKeys = []; 10 | Object.keys(module.default).forEach((key) => { 11 | if (!checkComponentKey(key)) { 12 | invalidCustomComponentKeys.push(key); 13 | hasFailed = true; 14 | } 15 | }); 16 | 17 | if (invalidCustomComponentKeys.length !== 0) { 18 | // eslint-disable-next-line no-console 19 | console.error( 20 | `ERROR: Invalid component declaration: ${invalidCustomComponentKeys} into 'src/components/custom/index.ts'. Their key must be declared using only lowercase and alphanumeric characters.`, 21 | ); 22 | } 23 | return hasFailed; 24 | } 25 | 26 | /** 27 | * Check the custom components in continuous integration 28 | * 29 | * npm run custom.check 30 | * 31 | */ 32 | async function check() { 33 | let hasFailed = false; 34 | 35 | hasFailed |= await checkDeclarationKey(); 36 | 37 | if (hasFailed) { 38 | process.exit(1); 39 | } 40 | } 41 | 42 | check(); 43 | -------------------------------------------------------------------------------- /src/ui/tools/generator.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import * as generatorPythonUi from "./generator_python_ui.mjs"; 3 | import * as generatorUiComponentJson from "./generator_ui_component_json.mjs"; 4 | import * as generatorStorybook from "./generator_storybook.mjs"; 5 | 6 | async function generate() { 7 | await generatorPythonUi.generate(); 8 | await generatorUiComponentJson.generate(); 9 | await generatorStorybook.generate(); 10 | } 11 | 12 | generate(); 13 | -------------------------------------------------------------------------------- /src/ui/tools/generator_ui_component_json.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | 6 | import { loadComponents } from "./core.mjs"; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | 11 | // eslint-disable-next-line prettier/prettier 12 | const componentsJsonPath = path.resolve(__dirname, "..", "components.codegen.json"); 13 | 14 | /** 15 | * Exports an inventory of Writer Framework components into json. 16 | * 17 | * @returns {Promise} 18 | */ 19 | export async function generate() { 20 | const components = await loadComponents(); 21 | 22 | // eslint-disable-next-line no-console 23 | console.log("Writing components JSON to", componentsJsonPath); 24 | await fs.writeFile(componentsJsonPath, JSON.stringify(components, null, 2)); 25 | } 26 | -------------------------------------------------------------------------------- /src/ui/tools/getComponents.ts: -------------------------------------------------------------------------------- 1 | import { generateCore } from "../src/core"; 2 | const wf = generateCore(); 3 | const types = wf.getSupportedComponentTypes(); 4 | const data = types.map((type) => { 5 | const def = wf.getComponentDefinition(type); 6 | return { type, ...def }; 7 | }); 8 | export { data }; 9 | -------------------------------------------------------------------------------- /src/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "resolveJsonModule": true, 8 | "importHelpers": true, 9 | "isolatedModules": true, 10 | "noEmit": true, 11 | "paths": { 12 | "@/*": ["./src/*"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ui/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | declare const WRITER_LIVE_CCT: string; 2 | -------------------------------------------------------------------------------- /src/ui/viteWriterPlugin.ts: -------------------------------------------------------------------------------- 1 | import { ResolvedConfig, Plugin } from "vite"; 2 | 3 | type WriterPluginConfig = ResolvedConfig & { 4 | includeWriterComponentPath?: boolean; 5 | }; 6 | 7 | export default (): Plugin => { 8 | let config: WriterPluginConfig; 9 | 10 | return { 11 | name: "writer-plugin", 12 | configResolved(resolvedConfig) { 13 | config = resolvedConfig as WriterPluginConfig; 14 | }, 15 | transform(code: string, id: string) { 16 | if (/vue&type=docs/.test(id)) { 17 | const docs = code 18 | .replaceAll(/'/g, "\\'") 19 | .replaceAll(/\n/g, "\\n") 20 | .replaceAll(/\r/g, "\\r") 21 | .trim() 22 | .replace(/^(\\n|\\t|[ \s])*/, "") 23 | .replace(/(\\n|\\t|[ \s])*$/, ""); 24 | return `export default Comp => { 25 | if(!Comp.writer) return; 26 | Comp.writer.docs = '${docs}'; 27 | }`; 28 | } 29 | if (/\.vue$/.test(id)) { 30 | if (config.includeWriterComponentPath === false) return; 31 | const fileRef = id.replace(`${__dirname}/`, ""); 32 | return `${code} 33 | if(_sfc_main.writer) _sfc_main.writer.fileRef = '${fileRef}'; 34 | `; 35 | } 36 | }, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/writer/abstract.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Abstract templates. 4 | 5 | They're used to register new component templates, based on existing frontend templates and a backend-provided component definition. 6 | 7 | """ 8 | 9 | from typing import Dict 10 | 11 | from writer.ss_types import AbstractTemplate 12 | 13 | templates:Dict[str, AbstractTemplate] = {} 14 | 15 | def register_abstract_template(type: str, abstract_template: AbstractTemplate): 16 | templates[type] = abstract_template 17 | -------------------------------------------------------------------------------- /src/writer/blocks/base_trigger.py: -------------------------------------------------------------------------------- 1 | from writer.blocks.base_block import BlueprintBlock 2 | from writer.ss_types import WriterConfigurationError 3 | 4 | 5 | class BlueprintTrigger(BlueprintBlock): 6 | def run(self): 7 | self.result = self.execution_environment.get("payload") 8 | 9 | if not self.result: 10 | try: 11 | self.result = self._get_field("defaultResult", True, None) 12 | except WriterConfigurationError: 13 | self.result = self._get_field("defaultResult", False, None) 14 | 15 | -------------------------------------------------------------------------------- /src/writer/crypto.py: -------------------------------------------------------------------------------- 1 | 2 | import hashlib 3 | import os 4 | 5 | from fastapi import HTTPException, Request 6 | 7 | HASH_SALT = "a9zHYfIeL0" 8 | 9 | def get_hash(message: str): 10 | base_hash = os.getenv("WRITER_SECRET_KEY") 11 | if not base_hash: 12 | raise ValueError("Environment variable WRITER_SECRET_KEY needs to be set up in" + \ 13 | "order to enable operations which require hash generation, such as creating async jobs.") 14 | assert HASH_SALT 15 | combined = base_hash + HASH_SALT + message 16 | return hashlib.sha256(combined.encode()).hexdigest() 17 | 18 | def verify_message_authorization_signature(message: str, request: Request): 19 | auth_header = request.headers.get("Authorization") 20 | if not auth_header: 21 | raise HTTPException(status_code=401, detail="Unauthorized. Token not specified.") 22 | if auth_header != f"Bearer {get_hash(message)}": 23 | raise HTTPException(status_code=403, detail="Forbidden. Incorrect token.") -------------------------------------------------------------------------------- /src/writer/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | plugins = pydantic.mypy -------------------------------------------------------------------------------- /src/writer/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/src/writer/py.typed -------------------------------------------------------------------------------- /tests/backend/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | test_app_dir = Path(__file__).resolve().parent / 'testapp' 4 | test_multiapp_dir = Path(__file__).resolve().parent / 'testmultiapp' 5 | test_basicauth_dir = Path(__file__).resolve().parent / 'testbasicauth' 6 | -------------------------------------------------------------------------------- /tests/backend/blocks/test_addtostatelist.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from writer.blocks.addtostatelist import AddToStateList 3 | 4 | 5 | def test_empty_list(session, runner): 6 | component = session.add_fake_component({"element": "my_list", "value": "my_value"}) 7 | block = AddToStateList(component, runner, {}) 8 | block.run() 9 | assert block.outcome == "success" 10 | assert session.session_state["my_list"] == ["my_value"] 11 | 12 | 13 | def test_non_empty_list(session, runner): 14 | session.session_state["my_list"] = ["a"] 15 | component = session.add_fake_component({"element": "my_list", "value": "b"}) 16 | block = AddToStateList(component, runner, {}) 17 | block.run() 18 | assert block.outcome == "success" 19 | assert session.session_state["my_list"] == ["a", "b"] 20 | 21 | 22 | def test_non_list_element(session, runner): 23 | session.session_state["my_element"] = "dog" 24 | component = session.add_fake_component({"element": "my_element", "value": "cat"}) 25 | block = AddToStateList(component, runner, {}) 26 | with pytest.raises(ValueError): 27 | block.run() 28 | assert block.outcome == "error" 29 | -------------------------------------------------------------------------------- /tests/backend/blocks/test_changepage.py: -------------------------------------------------------------------------------- 1 | import writer.core 2 | from writer.blocks.changepage import ChangePage 3 | from writer.core import WriterState 4 | 5 | 6 | def test_change_page(session, runner): 7 | session.session_state = WriterState({}, [{"type": "test", "payload": "Just a test"}]) 8 | component = session.add_fake_component({"pageKey": "secondaryPage"}) 9 | writer.core.Config.is_mail_enabled_for_log = True 10 | block = ChangePage(component, runner, {}) 11 | block.run() 12 | assert block.outcome == "success" 13 | latest_mail = session.session_state.mail[1] 14 | assert latest_mail.get("type") == "pageChange" 15 | assert latest_mail.get("payload") == "secondaryPage" 16 | -------------------------------------------------------------------------------- /tests/backend/blocks/test_code.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import types 3 | 4 | import pytest 5 | from writer.blocks.code import CodeBlock 6 | 7 | 8 | def test_run_code(session, runner, monkeypatch): 9 | fake_module = types.ModuleType("fake_writeruserapp") 10 | fake_module.my_fn = lambda: "Monkeypatched!" 11 | monkeypatch.setitem(sys.modules, "writeruserapp", fake_module) 12 | component = session.add_fake_component( 13 | { 14 | "code": """ 15 | print('hi testing stdout ' + str(test_thing_ee) + my_fn()) 16 | set_output("return " + str(test_thing_ee)) 17 | """ 18 | } 19 | ) 20 | block = CodeBlock(component, runner, {"test_thing_ee": 26}) 21 | block.run() 22 | assert block.outcome == "success" 23 | assert block.result == "return 26" 24 | 25 | 26 | def test_run_invalid_code(session, runner, monkeypatch): 27 | fake_module = types.ModuleType("fake_writeruserapp") 28 | fake_module.my_fn = lambda: "Monkeypatched!" 29 | monkeypatch.setitem(sys.modules, "writeruserapp", fake_module) 30 | 31 | component = session.add_fake_component( 32 | { 33 | "code": """ 34 | print(1/0) 35 | """ 36 | } 37 | ) 38 | block = CodeBlock(component, runner, {}) 39 | with pytest.raises(ZeroDivisionError): 40 | block.run() 41 | assert block.outcome == "error" 42 | -------------------------------------------------------------------------------- /tests/backend/blocks/test_foreach.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from writer.blocks.foreach import ForEach 3 | 4 | 5 | def test_basic_list(session, runner): 6 | component = session.add_fake_component({"items": "[2226, 2223]"}) 7 | block = ForEach(component, runner, {}) 8 | block.run() 9 | assert block.outcome == "success" 10 | assert 4 == block.result[0] 11 | assert 4 == block.result[1] 12 | 13 | 14 | def test_basic_dict(session, runner): 15 | component = session.add_fake_component({"items": '{"a": "zzz", "b": "fff"}'}) 16 | block = ForEach(component, runner, {}) 17 | block.run() 18 | assert block.outcome == "success" 19 | assert 4 == block.result.get("a") 20 | assert 4 == block.result.get("b") 21 | -------------------------------------------------------------------------------- /tests/backend/blocks/test_parsejson.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from writer.blocks.parsejson import ParseJSON 5 | from writer.blueprints import BlueprintRunner 6 | 7 | 8 | def test_valid_json(session, runner): 9 | component = session.add_fake_component({"plainText": '{ "hi": "yes" }'}) 10 | block = ParseJSON(component, runner, {}) 11 | block.run() 12 | assert block.outcome == "success" 13 | assert block.result.get("hi") == "yes" 14 | 15 | 16 | def test_invalid_json(session, runner): 17 | component = session.add_fake_component({"plainText": '{ "hi": yes }'}) 18 | block = ParseJSON(component, runner, {}) 19 | with pytest.raises(json.JSONDecodeError): 20 | block.run() 21 | 22 | assert block.outcome == "error" 23 | -------------------------------------------------------------------------------- /tests/backend/blocks/test_returnvalue.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from writer.blocks.returnvalue import ReturnValue 3 | from writer.core import WriterState 4 | 5 | 6 | def test_basic_return(session, runner): 7 | session.session_state = WriterState( 8 | { 9 | "animal": "marmot", 10 | } 11 | ) 12 | component = session.add_fake_component({"value": "@{animal}"}) 13 | block = ReturnValue(component, runner, {}) 14 | block.run() 15 | assert block.outcome == "success" 16 | assert block.result == block.return_value 17 | assert block.return_value == "marmot" 18 | 19 | 20 | def test_empty_return(session, runner): 21 | session.session_state = WriterState( 22 | { 23 | "animal": None, 24 | } 25 | ) 26 | component = session.add_fake_component({"value": "@{animal}"}) 27 | block = ReturnValue(component, runner, {}) 28 | 29 | with pytest.raises(ValueError): 30 | block.run() 31 | assert block.outcome == "error" 32 | -------------------------------------------------------------------------------- /tests/backend/blocks/test_runblueprint.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from writer.blocks.runblueprint import RunBlueprint 5 | 6 | 7 | def test_blueprint_that_does_not_exist(session, runner): 8 | component = session.add_fake_component( 9 | { 10 | "blueprintKey": "blueprintThatDoesNotExist", 11 | } 12 | ) 13 | block = RunBlueprint(component, runner, {}) 14 | with pytest.raises(ValueError): 15 | block.run() 16 | assert block.outcome == "error" 17 | 18 | 19 | def test_duplicator(session, runner): 20 | session.session_state["item_dict"] = json.loads('{"item": 23}') 21 | component = session.add_fake_component({"blueprintKey": "duplicator", "payload": "@{item_dict}"}) 22 | block = RunBlueprint(component, runner, {}) 23 | block.run() 24 | assert block.outcome == "success" 25 | assert block.result == 46 26 | 27 | 28 | def test_bad_blueprint(session, runner): 29 | component = session.add_fake_component({"blueprintKey": "boom"}) 30 | block = RunBlueprint(component, runner, {}) 31 | with pytest.raises(BaseException): 32 | block.run() 33 | assert block.outcome == "error" 34 | -------------------------------------------------------------------------------- /tests/backend/blocks/test_writernocodeapp.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from writer.blocks.writernocodeapp import WriterNoCodeApp 5 | 6 | 7 | def fake_generate_content(application_id, app_inputs): 8 | assert application_id == "123" 9 | 10 | name = app_inputs.get("name") 11 | animal = app_inputs.get("animal") 12 | 13 | return f"{name} the {animal} " 14 | 15 | 16 | def test_call_nocode_app(monkeypatch, session, runner, fake_client): 17 | monkeypatch.setattr("writer.ai.apps.generate_content", fake_generate_content) 18 | component = session.add_fake_component( 19 | {"appId": "123", "appInputs": json.dumps({"name": "Koko", "animal": "Hamster"})} 20 | ) 21 | block = WriterNoCodeApp(component, runner, {}) 22 | block.run() 23 | assert block.result == "Koko the Hamster" 24 | assert block.outcome == "success" 25 | 26 | 27 | def test_call_nocode_app_missing_appid(monkeypatch, session, runner, fake_client): 28 | monkeypatch.setattr("writer.ai.apps.generate_content", fake_generate_content) 29 | component = session.add_fake_component( 30 | {"appId": "", "appInputs": json.dumps({"name": "Momo", "animal": "Squirrel"})} 31 | ) 32 | block = WriterNoCodeApp(component, runner, {}) 33 | 34 | with pytest.raises(ValueError): 35 | block.run() 36 | -------------------------------------------------------------------------------- /tests/backend/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from typing import Union 3 | 4 | from . import file_fixtures 5 | 6 | fixture_path = os.path.realpath(os.path.dirname(__file__)) 7 | 8 | 9 | def load_fixture_content(path, format: file_fixtures.FileFormat = file_fixtures.FileFormat.auto) -> Union[str, dict, list]: 10 | """ 11 | Load the contents of a file from the fixture folder 12 | >>> c = load_fixture_content('obsoletes/ui_obsolete_visible.json') 13 | """ 14 | file_path = os.path.join(fixture_path, path) 15 | return file_fixtures.read(file_path, format) 16 | -------------------------------------------------------------------------------- /tests/backend/fixtures/components/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {}, "isCodeManaged": false, "position": 0} -------------------------------------------------------------------------------- /tests/backend/fixtures/core_ui_fixtures.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from writer.core_ui import Branch, Component, ComponentTree, ComponentTreeBranch 4 | 5 | 6 | def build_fake_component_tree(components: List[Component] = None, init_root=True): 7 | """ 8 | Builds a fake component tree for testing purposes. 9 | 10 | :param components: list of components to attach 11 | :param init_root: create a root component 12 | """ 13 | component_tree = ComponentTree([ComponentTreeBranch(Branch.bmc, freeze=False)]) 14 | if init_root: 15 | component_tree.attach(Component(id='root', parentId=None, type='root')) 16 | 17 | for component in components or []: 18 | component_tree.attach(component) 19 | 20 | return component_tree 21 | -------------------------------------------------------------------------------- /tests/backend/fixtures/obsoletes/ui_obsolete_visible.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": { 3 | "id": "root", 4 | "type": "root", 5 | "content": { 6 | "appName": "Hello", 7 | "emptinessColor": "#ffffff" 8 | }, 9 | "isCodeManaged": false, 10 | "position": 0 11 | }, 12 | "bb4d0e86-619e-4367-a180-be28ab6059f4": { 13 | "id": "root", 14 | "type": "page", 15 | "content": { 16 | "pageMode": "", 17 | "key": "main" 18 | }, 19 | "isCodeManaged": false, 20 | "position": 0, 21 | "parentId": "root", 22 | "visible": true 23 | }, 24 | "bb4d0e86-619e-4367-a180-be28abxxxx": { 25 | "id": "root", 26 | "type": "page", 27 | "content": { 28 | "pageMode": "", 29 | "key": "main" 30 | }, 31 | "isCodeManaged": false, 32 | "position": 0, 33 | "parentId": "root", 34 | "visible": "value" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/backend/test_audit_and_fix.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from writer import audit_and_fix 4 | 5 | from backend.fixtures import load_fixture_content 6 | 7 | 8 | def test_fix_components_should_fix_visible_fields(): 9 | # Given 10 | obsolete_components = load_fixture_content('obsoletes/ui_obsolete_visible.json') 11 | 12 | # When 13 | final_components = audit_and_fix.fix_components(obsolete_components) 14 | 15 | # Then 16 | assert "visible" not in final_components["root"] 17 | assert final_components['bb4d0e86-619e-4367-a180-be28ab6059f4']['visible'] == { 18 | 'expression': True, 19 | 'binding': "", 20 | 'reversed': False 21 | } 22 | assert final_components['bb4d0e86-619e-4367-a180-be28abxxxx']['visible'] == { 23 | 'expression': "custom", 24 | 'binding': "value", 25 | 'reversed': False 26 | } 27 | -------------------------------------------------------------------------------- /tests/backend/test_core_ui.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from writer import core_ui 4 | from writer.ss_types import ComponentDefinition 5 | 6 | from backend.fixtures import load_fixture_content 7 | 8 | 9 | def test_filter_components_by_should_retrieve_a_list_of_components_that_fit_the_parent(): 10 | # Given 11 | components_list: List[ComponentDefinition] = load_fixture_content('components/components-page-1.jsonl') 12 | components = {c['id']: c for c in components_list} 13 | 14 | # When 15 | components_filtered = core_ui.filter_components_by(components, '23bc1387-26ed-4ff2-8565-b027c2960c3c') 16 | 17 | # Then 18 | assert '23bc1387-26ed-4ff2-8565-b027c2960c3c' in components_filtered 19 | -------------------------------------------------------------------------------- /tests/backend/testapp/.wf/components-blueprints_blueprint-0-hywgzgfetx6rpiqz.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "hywgzgfetx6rpiqz", "type": "blueprints_blueprint", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "blueprints_root", "position": 0} 2 | {"id": "rbh725i69ilo6gsr", "type": "blueprints_setstate", "content": {"element": "test", "value": "test"}, "handlers": {}, "isCodeManaged": false, "outs": [{"outId": "success", "toNodeId": "f052suq3dgzb5np7"}, {"outId": "success", "toNodeId": "f052suq3dgzb5np7"}], "parentId": "hywgzgfetx6rpiqz", "position": 0, "x": 248, "y": 204} 3 | {"id": "mw5rz7ay5p8pg2fm", "type": "blueprints_writerclassification", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "hywgzgfetx6rpiqz", "position": 1, "x": 935, "y": 153} 4 | {"id": "3cy7f577x6xsijiq", "type": "blueprints_runblueprint", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "hywgzgfetx6rpiqz", "position": 2, "x": 645, "y": 328} 5 | {"id": "c0v1pnroo32gfsye", "type": "blueprints_httprequest", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "hywgzgfetx6rpiqz", "position": 3, "x": 497, "y": 80} 6 | -------------------------------------------------------------------------------- /tests/backend/testapp/.wf/components-blueprints_blueprint-1-8ffkuce0ermsm9dr.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "8ffkuce0ermsm9dr", "type": "blueprints_blueprint", "content": {"key": "blueprint2"}, "handlers": {}, "parentId": "blueprints_root", "position": 1} 2 | {"id": "6ymlyaewhil88bck", "type": "blueprints_returnvalue", "content": {"value": "987127"}, "handlers": {}, "parentId": "8ffkuce0ermsm9dr", "position": 0, "x": 408, "y": 172} 3 | -------------------------------------------------------------------------------- /tests/backend/testapp/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/backend/testapp/.wf/components-page-1-03796247-dec4-4671-85c9-16559789e013.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "03796247-dec4-4671-85c9-16559789e013", "type": "page", "content": {"key": "emptyPage@{notaref}"}, "handlers": {}, "isCodeManaged": false, "parentId": "root", "position": 1, "visible": {"binding": "", "expression": true, "reversed": false}} 2 | -------------------------------------------------------------------------------- /tests/backend/testapp/.wf/components-page-6-4b6f14b0-b2d9-43e7-8aba-8d3e939c1f83.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "4b6f14b0-b2d9-43e7-8aba-8d3e939c1f83", "type": "page", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "root", "position": 6, "visible": {"binding": "", "expression": true, "reversed": false}} 2 | {"id": "0cd59329-29c8-4887-beee-39794065221e", "type": "text", "content": {"text": "The counter is @{counter}"}, "handlers": {}, "isCodeManaged": false, "parentId": "4b6f14b0-b2d9-43e7-8aba-8d3e939c1f83", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 3 | {"id": "f811ca14-8915-443d-8dd3-77ae69fb80f4", "type": "repeater", "content": {"keyVariable": "itemId", "repeaterObject": "@{prog_languages}", "valueVariable": "item"}, "handlers": {}, "isCodeManaged": false, "parentId": "4b6f14b0-b2d9-43e7-8aba-8d3e939c1f83", "position": 1, "visible": {"binding": "", "expression": true, "reversed": false}} 4 | {"id": "2e688107-f865-419b-a07b-95103197e3fd", "type": "text", "content": {"text": "The id is @{itemId} and the name is @{item.name}"}, "handlers": {}, "isCodeManaged": false, "parentId": "f811ca14-8915-443d-8dd3-77ae69fb80f4", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 5 | -------------------------------------------------------------------------------- /tests/backend/testapp/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App"}, "isCodeManaged": false, "position": 0} -------------------------------------------------------------------------------- /tests/backend/testapp/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.3rc1" 3 | } -------------------------------------------------------------------------------- /tests/backend/testapp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | RUN apt-get update -y && \ 4 | apt upgrade -y && apt-get update && \ 5 | apt-get install -y python3-pip python3-dev curl sudo 6 | 7 | RUN mkdir /app 8 | 9 | COPY ./requirements.txt /app/requirements.txt 10 | 11 | WORKDIR /app 12 | 13 | RUN pip3 install writer==0.1.2 14 | RUN pip3 install -r requirements.txt 15 | 16 | COPY . /app 17 | 18 | ENTRYPOINT [ "writer", "run" ] 19 | 20 | EXPOSE 5000 21 | 22 | CMD [ ".", "--port", "5000", "--host", "0.0.0.0" ] -------------------------------------------------------------------------------- /tests/backend/testapp/assets/myfile.csv: -------------------------------------------------------------------------------- 1 | hello world -------------------------------------------------------------------------------- /tests/backend/testapp/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/tests/backend/testapp/requirements.txt -------------------------------------------------------------------------------- /tests/backend/testapp/server_setup.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import writer.serve 4 | 5 | if typing.TYPE_CHECKING: 6 | from fastapi import FastAPI 7 | 8 | # Returns the FastAPI application associated with the Writer Framework server. 9 | asgi_app: 'FastAPI' = writer.serve.app 10 | 11 | @asgi_app.get("/probes/healthcheck") 12 | def hello(): 13 | return "1" 14 | -------------------------------------------------------------------------------- /tests/backend/testapp/static/Banana.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/tests/backend/testapp/static/Banana.jpg -------------------------------------------------------------------------------- /tests/backend/testapp/static/Lettuce.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/tests/backend/testapp/static/Lettuce.jpg -------------------------------------------------------------------------------- /tests/backend/testapp/static/README.md: -------------------------------------------------------------------------------- 1 | # Serving static files 2 | 3 | You can use this folder to store files which will be served statically in the "/static" route. 4 | 5 | This is useful to store images and other files which will be served directly to the user of your application. 6 | 7 | For example, if you store an image named "myimage.jpg" in this folder, it'll be accessible as "static/myimage.jpg". 8 | You can use this relative route as the source in an Image component. 9 | 10 | # Favicon 11 | 12 | The favicon is served from this folder. Feel free to change it. Keep in mind that web browsers cache favicons differently 13 | than other resources. Hence, the favicon change might not reflect immediately. -------------------------------------------------------------------------------- /tests/backend/testapp/static/Spinach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/tests/backend/testapp/static/Spinach.jpg -------------------------------------------------------------------------------- /tests/backend/testapp/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/tests/backend/testapp/static/favicon.png -------------------------------------------------------------------------------- /tests/backend/testapp/static/file.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/tests/backend/testapp/static/file.js -------------------------------------------------------------------------------- /tests/backend/testbasicauth/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/backend/testbasicauth/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App"}, "isCodeManaged": false, "position": 0} -------------------------------------------------------------------------------- /tests/backend/testbasicauth/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.0rc1" 3 | } -------------------------------------------------------------------------------- /tests/backend/testbasicauth/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | initial_state = wf.init_state({ 4 | }) 5 | -------------------------------------------------------------------------------- /tests/backend/testbasicauth/server_setup.py: -------------------------------------------------------------------------------- 1 | import writer.auth 2 | import writer.serve 3 | 4 | _auth = writer.auth.BasicAuth( 5 | login='admin', 6 | password='admin', 7 | delay_after_failure=0 8 | ) 9 | 10 | writer.serve.register_auth(_auth) 11 | -------------------------------------------------------------------------------- /tests/backend/testbasicauth/static/file.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/tests/backend/testbasicauth/static/file.js -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app1/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app1/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App 1"}, "handlers": {}, "parentId": null, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app1/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.0rc1" 3 | } -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/tests/backend/testmultiapp/app1/__init__.py -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app1/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | # This is a placeholder to get you started or refresh your memory. 4 | # Delete it or adapt it as necessary. 5 | # Documentation is available at https://streamsync.cloud 6 | 7 | # Shows in the log when the app starts 8 | print("Hello world!") 9 | 10 | # Its name starts with _, so this function won't be exposed 11 | def _update_message(state): 12 | is_even = state["counter"] % 2 == 0 13 | message = ("+Even" if is_even else "-Odd") 14 | state["message"] = message 15 | 16 | def decrement(state): 17 | state["counter"] -= 1 18 | _update_message(state) 19 | 20 | def increment(state): 21 | state["counter"] += 1 22 | # Shows in the log when the event handler is run 23 | print("The counter has been incremented.") 24 | _update_message(state) 25 | 26 | # Initialise the state 27 | 28 | # "_my_private_element" won't be serialised or sent to the frontend, 29 | # because it starts with an underscore 30 | 31 | initial_state = wf.init_state({ 32 | "my_app": { 33 | "title": "My App 1" 34 | }, 35 | "_my_private_element": 1337, 36 | "message": None, 37 | "counter": 26, 38 | }) 39 | 40 | _update_message(initial_state) -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app1/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/tests/backend/testmultiapp/app1/static/favicon.png -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app2/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app2/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App 2"}, "handlers": {}, "parentId": null, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app2/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.0rc1" 3 | } -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/tests/backend/testmultiapp/app2/__init__.py -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app2/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | # This is a placeholder to get you started or refresh your memory. 4 | # Delete it or adapt it as necessary. 5 | # Documentation is available at https://streamsync.cloud 6 | 7 | # Shows in the log when the app starts 8 | print("Hello world!") 9 | 10 | # Its name starts with _, so this function won't be exposed 11 | def _update_message(state): 12 | is_even = state["counter"] % 2 == 0 13 | message = ("+Even" if is_even else "-Odd") 14 | state["message"] = message 15 | 16 | def decrement(state): 17 | state["counter"] -= 1 18 | _update_message(state) 19 | 20 | def increment(state): 21 | state["counter"] += 1 22 | # Shows in the log when the event handler is run 23 | print("The counter has been incremented.") 24 | _update_message(state) 25 | 26 | # Initialise the state 27 | 28 | # "_my_private_element" won't be serialised or sent to the frontend, 29 | # because it starts with an underscore 30 | 31 | initial_state = wf.init_state({ 32 | "my_app": { 33 | "title": "My App 2" 34 | }, 35 | "_my_private_element": 1337, 36 | "message": None, 37 | "counter": 26, 38 | }) 39 | 40 | _update_message(initial_state) -------------------------------------------------------------------------------- /tests/backend/testmultiapp/app2/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/writer/writer-framework/7e0df29edc8ab39ff426d38872762d00c5a7b92b/tests/backend/testmultiapp/app2/static/favicon.png -------------------------------------------------------------------------------- /tests/e2e/.gitignore: -------------------------------------------------------------------------------- 1 | runtime/ 2 | node_modules/ 3 | /test-results/ 4 | /playwright-report/ 5 | /blob-report/ 6 | /playwright/.cache/ 7 | -------------------------------------------------------------------------------- /tests/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "writer-e2e", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "e2e:setup": "playwright install --with-deps", 9 | "e2e": "playwright test --project=chromium --reporter=list", 10 | "e2e:ci": "playwright test", 11 | "e2e:chromium": "playwright test --project=chromium", 12 | "e2e:firefox": "playwright test --project=firefox", 13 | "e2e:webkit": "playwright test --project=webkit", 14 | "e2e:ui": "playwright test --project=chromium --ui", 15 | "e2e:grep": "playwright test --project=chromium --grep " 16 | }, 17 | "dependencies": { 18 | "express": "4.19.2", 19 | "http-proxy": "1.18.1", 20 | "writer-ui": "*" 21 | }, 22 | "devDependencies": { 23 | "@playwright/test": "^1.49.1", 24 | "nodemon": "3.1.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/e2e/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | export default defineConfig({ 4 | testDir: "./tests", 5 | fullyParallel: false, 6 | forbidOnly: !!process.env.CI, 7 | retries: process.env.CI ? 2 : 0, 8 | workers: process.env.CI ? 1 : undefined, 9 | reporter: "list", 10 | use: { 11 | baseURL: "http://127.0.0.1:7357", 12 | trace: "on-first-retry", 13 | }, 14 | 15 | projects: [ 16 | { 17 | name: "chromium", 18 | use: { ...devices["Desktop Chrome"] }, 19 | }, 20 | 21 | { 22 | name: "firefox", 23 | use: { ...devices["Desktop Firefox"] }, 24 | }, 25 | 26 | { 27 | name: "webkit", 28 | use: { ...devices["Desktop Safari"] }, 29 | }, 30 | ], 31 | 32 | webServer: { 33 | command: "npm start", 34 | url: "http://127.0.0.1:7357", 35 | reuseExistingServer: true, 36 | //stdout: 'pipe', 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /tests/e2e/presets/2columns/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/2columns/.wf/components-page-0-qlbz49xq2emx9ip4.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "qlbz49xq2emx9ip4", "type": "page", "content": {}, "handlers": {}, "parentId": "root", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 2 | {"id": "3fzi2hkb6fzfgf40", "type": "section", "content": {"title": ""}, "handlers": {}, "parentId": "qlbz49xq2emx9ip4", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 3 | {"id": "gm7nov9whz3q0i7l", "type": "columns", "content": {}, "handlers": {}, "parentId": "3fzi2hkb6fzfgf40", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 4 | {"id": "kj09cwig8j3336jc", "type": "column", "content": {"width": "1"}, "handlers": {}, "parentId": "gm7nov9whz3q0i7l", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 5 | {"id": "f5kw67b26pht1iys", "type": "column", "content": {"width": "1"}, "handlers": {}, "parentId": "gm7nov9whz3q0i7l", "position": 1, "visible": {"binding": "", "expression": true, "reversed": false}} 6 | -------------------------------------------------------------------------------- /tests/e2e/presets/2columns/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App"}, "handlers": {}, "parentId": null, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/2columns/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.0rc1" 3 | } -------------------------------------------------------------------------------- /tests/e2e/presets/2columns/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | initial_state = wf.init_state({ 4 | "counter": 26, 5 | }) 6 | -------------------------------------------------------------------------------- /tests/e2e/presets/2pages/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/2pages/.wf/components-page-0-qlbz49xq2emx9ip4.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "qlbz49xq2emx9ip4", "type": "page", "content": {"key": "page1"}, "handlers": {}, "parentId": "root", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 2 | -------------------------------------------------------------------------------- /tests/e2e/presets/2pages/.wf/components-page-1-6gnhb317w7k76uhw.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "6gnhb317w7k76uhw", "type": "page", "content": {"key": "page2"}, "handlers": {}, "parentId": "root", "position": 1, "visible": {"binding": "", "expression": true, "reversed": false}} 2 | -------------------------------------------------------------------------------- /tests/e2e/presets/2pages/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App"}, "handlers": {}, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/2pages/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.0rc1" 3 | } -------------------------------------------------------------------------------- /tests/e2e/presets/2pages/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | initial_state = wf.init_state({ 4 | "counter": 26, 5 | }) 6 | -------------------------------------------------------------------------------- /tests/e2e/presets/blueprints/.wf/components-blueprints_blueprint-0-auxjfi7lssb268ly.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "auxjfi7lssb268ly", "type": "blueprints_blueprint", "content": {"key": "handle_object"}, "handlers": {}, "isCodeManaged": false, "parentId": "blueprints_root", "position": 0} 2 | {"id": "8y56lmia3wu99jhl", "type": "blueprints_parsejson", "content": {"plainText": "{\"color\": \"@{payload}\", \"object\": \"@{context.item.object}\"}"}, "handlers": {}, "isCodeManaged": false, "outs": [{"toNodeId": "xy6vdzh2pm55alc0", "outId": "success"}], "parentId": "auxjfi7lssb268ly", "position": 0, "x": 150, "y": 319} 3 | {"id": "xy6vdzh2pm55alc0", "type": "blueprints_setstate", "content": {"alias": "Save the JSON", "element": "json_e2e", "value": "@{result}"}, "handlers": {}, "isCodeManaged": false, "outs": [{"toNodeId": "mve8ssvtk0pvw5yf", "outId": "success"}], "parentId": "auxjfi7lssb268ly", "position": 1, "x": 537, "y": 321} 4 | {"id": "mve8ssvtk0pvw5yf", "type": "blueprints_returnvalue", "content": {"alias": "", "value": "@{json_e2e}"}, "handlers": {}, "isCodeManaged": false, "parentId": "auxjfi7lssb268ly", "position": 2, "x": 885, "y": 331} 5 | -------------------------------------------------------------------------------- /tests/e2e/presets/blueprints/.wf/components-blueprints_blueprint-1-n20uom1t17z7c1h8.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "n20uom1t17z7c1h8", "type": "blueprints_blueprint", "content": {"key": "repeat_payload"}, "handlers": {}, "isCodeManaged": false, "parentId": "blueprints_root", "position": 1} 2 | {"id": "5rwx9ukywrkz2f8t", "type": "blueprints_returnvalue", "content": {"alias": "Repeat payload", "value": "@{payload}"}, "handlers": {}, "isCodeManaged": false, "parentId": "n20uom1t17z7c1h8", "position": 0, "x": 371, "y": 270} 3 | -------------------------------------------------------------------------------- /tests/e2e/presets/blueprints/.wf/components-blueprints_blueprint-2-bjhk2qqylt0ijn50.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "bjhk2qqylt0ijn50", "type": "blueprints_blueprint", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "blueprints_root", "position": 2} 2 | {"id": "60xs6i5w0ckdiymh", "type": "blueprints_runblueprint", "content": {"payload": "blue", "blueprintKey": "repeat_payload"}, "handlers": {}, "isCodeManaged": false, "outs": [{"toNodeId": "htzhnqlqe1u02l3o", "outId": "success"}], "parentId": "bjhk2qqylt0ijn50", "position": 0, "x": 244, "y": 282} 3 | {"id": "htzhnqlqe1u02l3o", "type": "blueprints_returnvalue", "content": {"value": "@{result}"}, "handlers": {}, "isCodeManaged": false, "parentId": "bjhk2qqylt0ijn50", "position": 1, "x": 622, "y": 287} 4 | -------------------------------------------------------------------------------- /tests/e2e/presets/blueprints/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/blueprints/.wf/components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "c0f99a9e-5004-4e75-a6c6-36f17490b134", "type": "page", "content": {"pageMode": "compact"}, "handlers": {}, "isCodeManaged": false, "parentId": "root", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 2 | {"id": "bebc5fe9-63a7-46a7-b0fa-62303555cfaf", "type": "header", "content": {"text": "Blueprints Test App"}, "handlers": {}, "isCodeManaged": false, "parentId": "c0f99a9e-5004-4e75-a6c6-36f17490b134", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 3 | {"id": "ixxb26ukbvr0sknw", "type": "repeater", "content": {"keyVariable": "itemId", "repeaterObject": "{ \"pl\": { \"object\": \"plant\" }, \"cu\": { \"object\": \"cup\" }}", "valueVariable": "item"}, "handlers": {}, "isCodeManaged": false, "parentId": "c0f99a9e-5004-4e75-a6c6-36f17490b134", "position": 1} 4 | {"id": "iftqnmjw8ipaknex", "type": "section", "content": {"title": "@{itemId}: @{item.object}"}, "handlers": {}, "isCodeManaged": false, "parentId": "ixxb26ukbvr0sknw", "position": 0} 5 | {"id": "7no34ag7gmwgm1rd", "type": "textinput", "content": {"label": "", "placeholder": "@{item.object}"}, "handlers": {"wf-change": "$runBlueprint_handle_object"}, "isCodeManaged": false, "parentId": "iftqnmjw8ipaknex", "position": 0} 6 | -------------------------------------------------------------------------------- /tests/e2e/presets/blueprints/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App"}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/blueprints/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.3rc1" 3 | } -------------------------------------------------------------------------------- /tests/e2e/presets/blueprints/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | wf.Config.feature_flags = ["blueprints"] -------------------------------------------------------------------------------- /tests/e2e/presets/empty_page/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/empty_page/.wf/components-page-0-qlbz49xq2emx9ip4.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "qlbz49xq2emx9ip4", "type": "page", "content": {}, "handlers": {}, "parentId": "root", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 2 | -------------------------------------------------------------------------------- /tests/e2e/presets/empty_page/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App"}, "handlers": {}, "parentId": null, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/empty_page/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.0rc1" 3 | } -------------------------------------------------------------------------------- /tests/e2e/presets/empty_page/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | initial_state = wf.init_state({ 4 | "counter": 26, 5 | }) 6 | -------------------------------------------------------------------------------- /tests/e2e/presets/jsonviewer/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/jsonviewer/.wf/components-page-0-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "bb4d0e86-619e-4367-a180-be28ab6059f4", "type": "page", "content": {"key": "main", "pageMode": ""}, "isCodeManaged": false, "parentId": "root", "position": 0} 2 | {"id": "fa1r81mv2rrdhfh9", "type": "jsonviewer", "content": {"data": "{\"name\":\"JSON Viewer\",\"description\":\"A JSON tree viewer where you can expand the keys.\",\"sample\":{\"description\":\"This sample is opened by default\",\"bool\":true,\"null\":null,\"list\":[1,\"two\",{\"key\":3}]},\"sampleClosed\":{\"description\":\"This sample is not opened by default\"},\"createdAt\":\"2024-08-13T20:45:15.668Z\"}", "initialDepth": "0"}, "handlers": {}, "isCodeManaged": false, "parentId": "bb4d0e86-619e-4367-a180-be28ab6059f4", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 3 | -------------------------------------------------------------------------------- /tests/e2e/presets/jsonviewer/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "Hello", "emptinessColor": "#ffffff"}, "isCodeManaged": false, "position": 0} -------------------------------------------------------------------------------- /tests/e2e/presets/jsonviewer/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.0rc1" 3 | } -------------------------------------------------------------------------------- /tests/e2e/presets/jsonviewer/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | initial_state = wf.init_state({ 4 | "json": { 5 | "bool": True, 6 | "array": [1,2,3,4], 7 | "obj": { 8 | "key": "value", 9 | "nested": { 10 | "foo": "bar" 11 | } 12 | } 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /tests/e2e/presets/low_code/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/low_code/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App"}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/low_code/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.0rc1" 3 | } -------------------------------------------------------------------------------- /tests/e2e/presets/low_code/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | initial_state = wf.init_state({ 4 | "value": "", 5 | }) 6 | 7 | def update_value(state, payload): 8 | state['value'] = payload 9 | 10 | def execute_test(state, ui): 11 | exec(state['code']) 12 | 13 | 14 | with wf.init_ui() as ui: 15 | with ui.find('initialization'): 16 | ui.Text({"text": "Initialization successful!"}); 17 | -------------------------------------------------------------------------------- /tests/e2e/presets/section/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/section/.wf/components-page-0-qlbz49xq2emx9ip4.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "qlbz49xq2emx9ip4", "type": "page", "content": {}, "handlers": {}, "parentId": "root", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 2 | {"id": "3fzi2hkb6fzfgf40", "type": "section", "content": {"title": ""}, "handlers": {}, "parentId": "qlbz49xq2emx9ip4", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 3 | -------------------------------------------------------------------------------- /tests/e2e/presets/section/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App"}, "handlers": {}, "parentId": null, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/section/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.0rc1" 3 | } -------------------------------------------------------------------------------- /tests/e2e/presets/section/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | initial_state = wf.init_state({ 4 | "counter": 26, 5 | }) 6 | -------------------------------------------------------------------------------- /tests/e2e/presets/state/.wf/components-blueprints_root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "blueprints_root", "type": "blueprints_root", "content": {}, "handlers": {}, "isCodeManaged": false, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/state/.wf/components-page-0-qlbz49xq2emx9ip4.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "qlbz49xq2emx9ip4", "type": "page", "content": {}, "handlers": {}, "parentId": "root", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 2 | {"id": "0jbl2x7sl5gbjdnl", "type": "section", "content": {"title": "Section Title"}, "handlers": {}, "parentId": "qlbz49xq2emx9ip4", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 3 | {"id": "kdoy3ak62vcre8or", "type": "text", "content": {"cssClasses": "TestResult", "text": "@{data.counter}"}, "handlers": {}, "parentId": "0jbl2x7sl5gbjdnl", "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} 4 | {"id": "1ybermv0ttvw4lhk", "type": "textareainput", "binding": {"eventType": "wf-change", "stateRef": "code"}, "content": {"cssClasses": "TestInput", "label": "Input Label", "rows": "5"}, "handlers": {}, "parentId": "0jbl2x7sl5gbjdnl", "position": 1, "visible": {"binding": "", "expression": true, "reversed": false}} 5 | {"id": "byk3srfwyj1hn4wl", "type": "button", "content": {"cssClasses": "TestExec", "text": "Run test"}, "handlers": {"wf-click": "execute_test"}, "parentId": "0jbl2x7sl5gbjdnl", "position": 2, "visible": {"binding": "", "expression": true, "reversed": false}} 6 | -------------------------------------------------------------------------------- /tests/e2e/presets/state/.wf/components-root.jsonl: -------------------------------------------------------------------------------- 1 | {"id": "root", "type": "root", "content": {"appName": "My App"}, "handlers": {}, "parentId": null, "position": 0, "visible": {"binding": "", "expression": true, "reversed": false}} -------------------------------------------------------------------------------- /tests/e2e/presets/state/.wf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "writer_version": "0.8.0rc1" 3 | } -------------------------------------------------------------------------------- /tests/e2e/presets/state/main.py: -------------------------------------------------------------------------------- 1 | import writer as wf 2 | 3 | initial_state = wf.init_state({ 4 | "types": { 5 | "none": None, 6 | "string": "Hello, World!", 7 | "integer": 42, 8 | "float": 3.14, 9 | }, 10 | "counter": 26, 11 | "list": ["A", "B", "C"], 12 | "dict": {"a": 1, "b": 2}, 13 | "nested": { 14 | "a": 1, 15 | "b": 2, 16 | "c": { 17 | "d": 3, 18 | "e": 4 19 | } 20 | }, 21 | 22 | "nested_list": [ 23 | [1, 2, 3], 24 | [4, 5, 6], 25 | [7, 8, 9] 26 | ], 27 | "code": "" 28 | }) 29 | 30 | def execute_test(state): 31 | exec(state['code']) 32 | 33 | -------------------------------------------------------------------------------- /tests/e2e/tests/button.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test.describe("button", () => { 4 | const TYPE = "button"; 5 | const COMPONENT_LOCATOR = `button.CoreButton.component`; 6 | let url: string; 7 | 8 | test.beforeAll(async ({ request }) => { 9 | const response = await request.post(`/preset/section`); 10 | expect(response.ok()).toBeTruthy(); 11 | ({ url } = await response.json()); 12 | }); 13 | 14 | test.afterAll(async ({ request }) => { 15 | await request.delete(url); 16 | }); 17 | 18 | test.beforeEach(async ({ page }) => { 19 | await page.goto(url, { waitUntil: "domcontentloaded" }); 20 | test.setTimeout(5000); 21 | }); 22 | 23 | test("configure", async ({ page }) => { 24 | await page.locator(`[data-automation-action="sidebar-add"]`).click(); 25 | await page 26 | .locator(`.BuilderSidebarToolkit [data-component-type="${TYPE}"]`) 27 | .dragTo(page.locator(".CoreSection .ChildlessPlaceholder")); 28 | await page.locator(COMPONENT_LOCATOR).click(); 29 | await page 30 | .locator('.BuilderFieldsText[data-automation-key="text"] input') 31 | .fill("Hello, World!"); 32 | await expect(page.locator(COMPONENT_LOCATOR)).toContainText( 33 | "Hello, World!", 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/e2e/tests/drag.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test.describe("drag", () => { 4 | const COMPONENT_LOCATOR = `section.CoreSection.component`; 5 | const COLUMN = ".CoreColumns .CoreColumn:nth-child(1 of .CoreColumn)"; 6 | let url: string; 7 | 8 | test.beforeAll(async ({request}) => { 9 | const response = await request.post(`/preset/2columns`); 10 | expect(response.ok()).toBeTruthy(); 11 | ({url} = await response.json()); 12 | }); 13 | 14 | test.afterAll(async ({request}) => { 15 | await request.delete(url); 16 | }); 17 | 18 | test.beforeEach(async ({ page }) => { 19 | await page.goto(url, {waitUntil: "domcontentloaded"}); 20 | }); 21 | 22 | test("drag and drop component into itself", async ({ page }) => { 23 | await page 24 | .locator(COMPONENT_LOCATOR) 25 | .dragTo(page.locator(COLUMN)); 26 | await expect(page.locator(COLUMN)).toHaveCount(1); 27 | await expect(page.locator(COMPONENT_LOCATOR)).toHaveCount(1); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/e2e/tests/image.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test.describe("image", () => { 4 | const TYPE = "image"; 5 | const COMPONENT_LOCATOR = `div.CoreImage.component`; 6 | let url: string; 7 | 8 | test.beforeAll(async ({ request }) => { 9 | const response = await request.post(`/preset/section`); 10 | expect(response.ok()).toBeTruthy(); 11 | ({ url } = await response.json()); 12 | }); 13 | 14 | test.afterAll(async ({ request }) => { 15 | await request.delete(url); 16 | }); 17 | 18 | test.beforeEach(async ({ page }) => { 19 | await page.goto(url, { waitUntil: "domcontentloaded" }); 20 | }); 21 | 22 | test("configure", async ({ page }) => { 23 | await page.locator(`[data-automation-action="sidebar-add"]`).click(); 24 | await page 25 | .locator(`.BuilderSidebarToolkit [data-component-type="${TYPE}"]`) 26 | .dragTo(page.locator(".CoreSection .ChildlessPlaceholder")); 27 | await page.locator(COMPONENT_LOCATOR).click(); 28 | await page 29 | .locator('.BuilderFieldsText[data-automation-key="caption"] input') 30 | .fill("Hello, World!"); 31 | await expect(page.locator(COMPONENT_LOCATOR)).toContainText( 32 | "Hello, World!", 33 | ); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/e2e/tests/jsonviewer.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test.describe("JSON viewer", () => { 4 | let url: string; 5 | 6 | test.beforeAll(async ({ request }) => { 7 | const response = await request.post(`/preset/jsonviewer`); 8 | expect(response.ok()).toBeTruthy(); 9 | ({ url } = await response.json()); 10 | }); 11 | 12 | test.afterAll(async ({ request }) => { 13 | await request.delete(url); 14 | }); 15 | 16 | test.beforeEach(async ({ page }) => { 17 | await page.goto(url, {waitUntil: "domcontentloaded"}); 18 | test.setTimeout(5000); 19 | }); 20 | 21 | test("should controle the depth open", async ({ page }) => { 22 | await page.locator(".CoreJsonViewer").click(); 23 | await page.locator(".BuilderTemplateInput").first().click(); 24 | 25 | expect(await page.locator(".CoreJsonViewer details[open]").count()).toBe(0); 26 | 27 | await page.getByRole("combobox").first().fill("1"); 28 | expect(await page.locator(".CoreJsonViewer details[open]").count()).toBe(1); 29 | 30 | await page.getByRole("combobox").first().fill("-1"); 31 | expect(await page.locator(".CoreJsonViewer details[open]").count()).toBe(5); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/e2e/tests/sidebar.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test.describe("sidebar", () => { 4 | let url: string; 5 | 6 | test.beforeAll(async ({ request }) => { 7 | const response = await request.post(`/preset/empty_page`); 8 | expect(response.ok()).toBeTruthy(); 9 | ({ url } = await response.json()); 10 | }); 11 | 12 | test.afterAll(async ({ request }) => { 13 | await request.delete(url); 14 | }); 15 | 16 | test.beforeEach(async ({ page }) => { 17 | await page.goto(url, { waitUntil: "domcontentloaded" }); 18 | test.setTimeout(5000); 19 | }); 20 | 21 | test.describe("Toolkit", () => { 22 | test("should filter", async ({ page }) => { 23 | await page.locator(`[data-automation-action="sidebar-add"]`).click(); 24 | // click on icon to begin search 25 | const panel = page.locator(`.BuilderSidebarToolkit`); 26 | 27 | // search a button 28 | await panel.locator(`input`).fill("button"); 29 | 30 | // should have only one result 31 | expect(await panel.locator(`.tool`).count()).toBe(1); 32 | 33 | // search a button 34 | await panel.locator(`input`).fill(""); 35 | 36 | // should reset the search 37 | expect(await panel.locator(`.tool`).count()).not.toBe(1); 38 | }); 39 | }); 40 | }); 41 | --------------------------------------------------------------------------------