diff --git a/docs/framework/custom-components.mdx b/docs/framework/custom-components.mdx index c8540d553..23152de51 100644 --- a/docs/framework/custom-components.mdx +++ b/docs/framework/custom-components.mdx @@ -2,13 +2,15 @@ title: "Custom components" --- -It's possible to extend Framework with custom component templates. +It's possible to extend Framework with custom component templates. They're developed using Vue 3 and TypeScript. Once transpiled, they can be used by copying them to the `extensions/` folder of any project. - -Custom components behave exactly like built-in ones. -They are just as performant, can contain other components, and offer the same the Builder experience. They only differ from built-in components in the way that they're bundled and imported. + + Custom components behave exactly like built-in ones. They are just as + performant, can contain other components, and offer the same the Builder + experience. They only differ from built-in components in the way that they're + bundled and imported. ## Architecture @@ -21,7 +23,7 @@ Extensions and custom templates are currently synonyms, but this might change in ![Custom Components - Architecture](/framework/images/custom-components.architecture.png) -Dependencies are [provided](https://vuejs.org/api/composition-api-dependency-injection.html) using injection symbols and can be _injected_ to be used by the component template. These include `evaluatedFields`, which contain the current values of the editable fields. Injected dependencies are fully typed, making development easier. +Dependencies are [provided](https://vuejs.org/api/composition-api-dependency-injection.html) using injection symbols and can be _injected_ to be used by the component template. These include `evaluatedFields`, which contain the current values of the editable fields. Injected dependencies are fully typed, making development easier. [Rollup's external feature](https://rollupjs.org/configuration-options/#external), invoked via Vite, allows for extensions to be compiled without dependencies and link those during runtime. Therefore, extensions aren't bundled to be standalone, but rather to work as a piece of a puzzle. @@ -35,7 +37,7 @@ Framework component templates are purely front-end. They are Vue 3 templates tha ### Simple example -This example shows a template for _Bubble Message_, a simple demo component with one editable field, `text`. +This example shows a template for _Bubble Message_, a simple demo component with one editable field, `text`. ```js +../base/BaseContainer.vue diff --git a/src/ui/src/core_components/layout/CoreSteps.vue b/src/ui/src/components/core/layout/CoreSteps.vue similarity index 91% rename from src/ui/src/core_components/layout/CoreSteps.vue rename to src/ui/src/components/core/layout/CoreSteps.vue index 765273061..e521135cc 100644 --- a/src/ui/src/core_components/layout/CoreSteps.vue +++ b/src/ui/src/components/core/layout/CoreSteps.vue @@ -25,7 +25,7 @@ import { buttonTextColor, buttonShadow, cssClasses, -} from "../../renderer/sharedStyleFields"; +} from "@/renderer/sharedStyleFields"; const description = "A container component for displaying Step components, allowing you to implement a stepped workflow."; @@ -53,8 +53,8 @@ export default { diff --git a/src/ui/src/core/auditAndFix.ts b/src/ui/src/core/auditAndFix.ts index a35276725..8760b7074 100644 --- a/src/ui/src/core/auditAndFix.ts +++ b/src/ui/src/core/auditAndFix.ts @@ -2,7 +2,7 @@ import { Component, ComponentMap, WriterComponentDefinition, -} from "../writerTypes"; +} from "@/writerTypes"; import { getComponentDefinition } from "./templateMap"; /** diff --git a/src/ui/src/core/index.ts b/src/ui/src/core/index.ts index 04c434292..fdda46bb0 100644 --- a/src/ui/src/core/index.ts +++ b/src/ui/src/core/index.ts @@ -1,16 +1,18 @@ /* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ref, Ref } from "vue"; +import { readonly, ref, Ref, shallowRef } from "vue"; import { + AbstractTemplate, Component, ComponentMap, InstancePath, MailItem, UserFunction, -} from "../writerTypes"; +} from "@/writerTypes"; import { getSupportedComponentTypes, getComponentDefinition, + registerAbstractComponentTemplate, } from "./templateMap"; import * as typeHierarchy from "./typeHierarchy"; import { auditAndFixComponents } from "./auditAndFix"; @@ -24,6 +26,7 @@ export function generateCore() { let sessionId: string = null; const sessionTimestamp: Ref = ref(null); const mode: Ref<"run" | "edit"> = ref(null); + const featureFlags = shallowRef([]); const runCode: Ref = ref(null); const components: Ref = ref({}); const userFunctions: Ref = ref([]); @@ -98,7 +101,9 @@ export function generateCore() { collateMail(initData.mail); sessionId = initData.sessionId; sessionTimestamp.value = new Date().getTime(); - + featureFlags.value = initData.featureFlags; + loadAbstractTemplates(initData.abstractTemplates); + // Only returned for edit (Builder) mode userFunctions.value = initData.userFunctions; @@ -112,6 +117,16 @@ export function generateCore() { await sendComponentUpdate(); } + function loadAbstractTemplates( + abstractTemplates: Record, + ) { + Object.entries(abstractTemplates ?? {}).forEach( + ([type, abstractTemplate]) => { + registerAbstractComponentTemplate(type, abstractTemplate); + }, + ); + } + function getSessionTimestamp() { return sessionTimestamp.value; } @@ -594,6 +609,7 @@ export function generateCore() { getSessionTimestamp, getUserState, isChildOf, + featureFlags: readonly(featureFlags), }; return core; diff --git a/src/ui/src/core/templateMap.ts b/src/ui/src/core/templateMap.ts index 23b7d91ad..facc6330f 100644 --- a/src/ui/src/core/templateMap.ts +++ b/src/ui/src/core/templateMap.ts @@ -1,69 +1,72 @@ // Maps Writer Framework component types to renderable Vue components // content -import CoreDataframe from "../core_components/content/CoreDataframe.vue"; -import CoreHeading from "../core_components/content/CoreHeading.vue"; -import CoreIcon from "../core_components/content/CoreIcon.vue"; -import CoreImage from "../core_components/content/CoreImage.vue"; -import CoreMessage from "../core_components/content/CoreMessage.vue"; -import CoreMetric from "../core_components/content/CoreMetric.vue"; -import CorePlotlyGraph from "../core_components/content/CorePlotlyGraph.vue"; -import CoreText from "../core_components/content/CoreText.vue"; -import CoreVegaLiteChart from "../core_components/content/CoreVegaLiteChart.vue"; -import CoreVideoPlayer from "../core_components/content/CoreVideoPlayer.vue"; -import CoreLink from "../core_components/content/CoreLink.vue"; -import CoreChatbot from "../core_components/content/CoreChatbot.vue"; -import CoreTags from "../core_components/content/CoreTags.vue"; -import CoreAvatar from "../core_components/content/CoreAvatar.vue"; -import CoreAnnotatedText from "../core_components/content/CoreAnnotatedText.vue"; -import CoreJsonViewer from "../core_components/content/CoreJsonViewer.vue"; +import CoreDataframe from "../components/core/content/CoreDataframe.vue"; +import CoreHeading from "../components/core/content/CoreHeading.vue"; +import CoreIcon from "../components/core/content/CoreIcon.vue"; +import CoreImage from "../components/core/content/CoreImage.vue"; +import CoreMessage from "../components/core/content/CoreMessage.vue"; +import CoreMetric from "../components/core/content/CoreMetric.vue"; +import CorePlotlyGraph from "../components/core/content/CorePlotlyGraph.vue"; +import CoreText from "../components/core/content/CoreText.vue"; +import CoreVegaLiteChart from "../components/core/content/CoreVegaLiteChart.vue"; +import CoreVideoPlayer from "../components/core/content/CoreVideoPlayer.vue"; +import CoreLink from "../components/core/content/CoreLink.vue"; +import CoreChatbot from "../components/core/content/CoreChatbot.vue"; +import CoreTags from "../components/core/content/CoreTags.vue"; +import CoreAvatar from "../components/core/content/CoreAvatar.vue"; +import CoreAnnotatedText from "../components/core/content/CoreAnnotatedText.vue"; +import CoreJsonViewer from "../components/core/content/CoreJsonViewer.vue"; // input -import CoreCheckboxInput from "../core_components/input/CoreCheckboxInput.vue"; -import CoreColorInput from "../core_components/input/CoreColorInput.vue"; -import CoreDateInput from "../core_components/input/CoreDateInput.vue"; -import CoreDropdownInput from "../core_components/input/CoreDropdownInput.vue"; -import CoreFileInput from "../core_components/input/CoreFileInput.vue"; -import CoreMultiselectInput from "../core_components/input/CoreMultiselectInput.vue"; -import CoreNumberInput from "../core_components/input/CoreNumberInput.vue"; -import CoreRadioInput from "../core_components/input/CoreRadioInput.vue"; -import CoreSelectInput from "../core_components/input/CoreSelectInput.vue"; -import CoreSliderInput from "../core_components/input/CoreSliderInput.vue"; -import CoreTextInput from "../core_components/input/CoreTextInput.vue"; -import CoreTextareaInput from "../core_components/input/CoreTextareaInput.vue"; -import CoreTimeInput from "../core_components/input/CoreTimeInput.vue"; -import CoreRating from "../core_components/input/CoreRatingInput.vue"; -import CoreSwitchInput from "../core_components/input/CoreSwitchInput.vue"; +import CoreCheckboxInput from "../components/core/input/CoreCheckboxInput.vue"; +import CoreColorInput from "../components/core/input/CoreColorInput.vue"; +import CoreDateInput from "../components/core/input/CoreDateInput.vue"; +import CoreDropdownInput from "../components/core/input/CoreDropdownInput.vue"; +import CoreFileInput from "../components/core/input/CoreFileInput.vue"; +import CoreMultiselectInput from "../components/core/input/CoreMultiselectInput.vue"; +import CoreNumberInput from "../components/core/input/CoreNumberInput.vue"; +import CoreRadioInput from "../components/core/input/CoreRadioInput.vue"; +import CoreSelectInput from "../components/core/input/CoreSelectInput.vue"; +import CoreSliderInput from "../components/core/input/CoreSliderInput.vue"; +import CoreTextInput from "../components/core/input/CoreTextInput.vue"; +import CoreTextareaInput from "../components/core/input/CoreTextareaInput.vue"; +import CoreTimeInput from "../components/core/input/CoreTimeInput.vue"; +import CoreRating from "../components/core/input/CoreRatingInput.vue"; +import CoreSwitchInput from "../components/core/input/CoreSwitchInput.vue"; // layout -import CoreColumn from "../core_components/layout/CoreColumn.vue"; -import CoreColumns from "../core_components/layout/CoreColumns.vue"; -import CoreHeader from "../core_components/layout/CoreHeader.vue"; -import CoreHorizontalStack from "../core_components/layout/CoreHorizontalStack.vue"; -import CoreSection from "../core_components/layout/CoreSection.vue"; -import CoreSeparator from "../core_components/layout/CoreSeparator.vue"; -import CoreSidebar from "../core_components/layout/CoreSidebar.vue"; -import CoreTab from "../core_components/layout/CoreTab.vue"; -import CoreTabs from "../core_components/layout/CoreTabs.vue"; -import CoreStep from "../core_components/layout/CoreStep.vue"; -import CoreSteps from "../core_components/layout/CoreSteps.vue"; +import CoreColumn from "../components/core/layout/CoreColumn.vue"; +import CoreColumns from "../components/core/layout/CoreColumns.vue"; +import CoreHeader from "../components/core/layout/CoreHeader.vue"; +import CoreHorizontalStack from "../components/core/layout/CoreHorizontalStack.vue"; +import CoreSection from "../components/core/layout/CoreSection.vue"; +import CoreSeparator from "../components/core/layout/CoreSeparator.vue"; +import CoreSidebar from "../components/core/layout/CoreSidebar.vue"; +import CoreTab from "../components/core/layout/CoreTab.vue"; +import CoreTabs from "../components/core/layout/CoreTabs.vue"; +import CoreStep from "../components/core/layout/CoreStep.vue"; +import CoreSteps from "../components/core/layout/CoreSteps.vue"; // other -import CoreButton from "../core_components/other/CoreButton.vue"; -import CoreHtml from "../core_components/other/CoreHtml.vue"; -import CorePagination from "../core_components/other/CorePagination.vue"; -import CoreRepeater from "../core_components/other/CoreRepeater.vue"; -import CoreTimer from "../core_components/other/CoreTimer.vue"; -import CoreWebcamCapture from "../core_components/other/CoreWebcamCapture.vue"; -import CoreReuse from "../core_components/other/CoreReuse.vue"; +import CoreButton from "../components/core/other/CoreButton.vue"; +import CoreHtml from "../components/core/other/CoreHtml.vue"; +import CorePagination from "../components/core/other/CorePagination.vue"; +import CoreRepeater from "../components/core/other/CoreRepeater.vue"; +import CoreTimer from "../components/core/other/CoreTimer.vue"; +import CoreWebcamCapture from "../components/core/other/CoreWebcamCapture.vue"; +import CoreReuse from "../components/core/other/CoreReuse.vue"; // embed -import CorePDF from "../core_components/embed/CorePDF.vue"; -import CoreIFrame from "../core_components/embed/CoreIFrame.vue"; -import CoreGoogleMaps from "../core_components/embed/CoreGoogleMaps.vue"; +import CorePDF from "../components/core/embed/CorePDF.vue"; +import CoreIFrame from "../components/core/embed/CoreIFrame.vue"; +import CoreGoogleMaps from "../components/core/embed/CoreGoogleMaps.vue"; // root -import CorePage from "../core_components/root/CorePage.vue"; -import CoreRoot from "../core_components/root/CoreRoot.vue"; +import CorePage from "../components/core/root/CorePage.vue"; +import CoreRoot from "../components/core/root/CoreRoot.vue"; +import CoreMapbox from "../components/core/embed/CoreMapbox.vue"; -import CoreMapbox from "../core_components/embed/CoreMapbox.vue"; +// WORKFLOWS -import { WriterComponentDefinition } from "../writerTypes"; +import WorkflowsNode from "../components/workflows/abstract/WorkflowsNode.vue"; + +import { AbstractTemplate, WriterComponentDefinition } from "@/writerTypes"; import { h } from "vue"; const templateMap = { @@ -122,8 +125,11 @@ const templateMap = { avatar: CoreAvatar, annotatedtext: CoreAnnotatedText, jsonviewer: CoreJsonViewer, + workflowsnode: WorkflowsNode, }; +const abstractTemplateMap: Record = {}; + if (WRITER_LIVE_CCT === "yes") { /* Assigns the components in custom_components to the template map, @@ -163,8 +169,29 @@ function fallbackTemplate(type: string) { }; } +function getMergedAbstractTemplate(type: string) { + const template = abstractTemplateMap[type]; + if (!template) return; + const baseType = template.baseType; + return { + ...templateMap[baseType], + writer: { + ...templateMap[baseType].writer, + ...abstractTemplateMap[type].writer, + fields: { + ...templateMap[baseType].writer?.fields, + ...abstractTemplateMap[type].writer?.fields, + }, + }, + }; +} + export function getTemplate(type: string) { - return templateMap[type] ?? fallbackTemplate(type); + return ( + getMergedAbstractTemplate(type) ?? + templateMap[type] ?? + fallbackTemplate(type) + ); } export function getComponentDefinition( @@ -174,11 +201,18 @@ export function getComponentDefinition( } export function getSupportedComponentTypes() { - return Object.keys(templateMap); + return [...Object.keys(templateMap), ...Object.keys(abstractTemplateMap)]; } export function registerComponentTemplate(type: string, vueComponent: any) { templateMap[type] = vueComponent; } +export function registerAbstractComponentTemplate( + type: string, + abstractTemplate: AbstractTemplate, +) { + abstractTemplateMap[type] = abstractTemplate; +} + export default templateMap; diff --git a/src/ui/src/core/typeHierarchy.ts b/src/ui/src/core/typeHierarchy.ts index 1a687cf6e..f2065b0fa 100644 --- a/src/ui/src/core/typeHierarchy.ts +++ b/src/ui/src/core/typeHierarchy.ts @@ -1,4 +1,4 @@ -import { Component, ComponentMap } from "../writerTypes"; +import { Component, ComponentMap } from "@/writerTypes"; import { getComponentDefinition, getSupportedComponentTypes, diff --git a/src/ui/src/custom_components/BubbleMessage.vue b/src/ui/src/custom_components/BubbleMessage.vue index 0f2a5bcdb..363f7e1f3 100644 --- a/src/ui/src/custom_components/BubbleMessage.vue +++ b/src/ui/src/custom_components/BubbleMessage.vue @@ -32,8 +32,8 @@ export default { }; diff --git a/src/ui/src/renderer/ComponentRenderer.vue b/src/ui/src/renderer/ComponentRenderer.vue index 69f49dbf9..0b2d1e5a5 100644 --- a/src/ui/src/renderer/ComponentRenderer.vue +++ b/src/ui/src/renderer/ComponentRenderer.vue @@ -21,10 +21,10 @@