diff --git a/app/web/src/components/FuncEditor/FuncList.vue b/app/web/src/components/FuncEditor/FuncList.vue
index 209189e962..e321b98468 100644
--- a/app/web/src/components/FuncEditor/FuncList.vue
+++ b/app/web/src/components/FuncEditor/FuncList.vue
@@ -57,7 +57,7 @@ import {
} from "@/api/sdf/dal/func";
import { useFeatureFlagsStore } from "@/store/feature_flags.store";
-const ffStore = useFeatureFlagsStore();
+const featureFlagsStore = useFeatureFlagsStore();
// filtering out a func type if FF for mgmt functions is off
// When you use an enum as keys in a record
@@ -66,7 +66,10 @@ const ffStore = useFeatureFlagsStore();
// So, I can Partial to make all keys optional
const funcKindOptions: Partial = {};
Object.entries(CUSTOMIZABLE_FUNC_TYPES).forEach(([key, value]) => {
- if (!ffStore.MANAGEMENT_FUNCTIONS && key === CustomizableFuncKind.Management)
+ if (
+ !featureFlagsStore.MANAGEMENT_FUNCTIONS &&
+ key === CustomizableFuncKind.Management
+ )
return;
funcKindOptions[key] = value;
});
diff --git a/app/web/src/components/ModelingDiagram/DiagramGroup.vue b/app/web/src/components/ModelingDiagram/DiagramGroup.vue
index ac2c2bd520..3f8426c345 100644
--- a/app/web/src/components/ModelingDiagram/DiagramGroup.vue
+++ b/app/web/src/components/ModelingDiagram/DiagramGroup.vue
@@ -362,6 +362,7 @@
/>
+
+
+
+
@@ -496,9 +513,13 @@ const viewStore = useViewsStore();
const irect = computed(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const r = viewStore.groups[props.group.def.id]!;
- r.width = Math.max(r.width, MIN_NODE_DIMENSION);
- r.height = Math.max(r.height, MIN_NODE_DIMENSION);
- return r;
+
+ return {
+ x: r.x,
+ y: r.y,
+ width: Math.max(r.width, MIN_NODE_DIMENSION),
+ height: Math.max(r.height, MIN_NODE_DIMENSION),
+ };
});
const isDeleted = computed(
diff --git a/app/web/src/components/ModelingDiagram/DiagramNode.vue b/app/web/src/components/ModelingDiagram/DiagramNode.vue
index 78c624aa28..e210850878 100644
--- a/app/web/src/components/ModelingDiagram/DiagramNode.vue
+++ b/app/web/src/components/ModelingDiagram/DiagramNode.vue
@@ -265,16 +265,21 @@
-
-
+
+
@@ -309,7 +314,6 @@ import {
NODE_TITLE_HEADER_MARGIN_RIGHT as NODE_HEADER_MARGIN_RIGHT,
NODE_HEADER_HEIGHT,
NODE_HEADER_TEXT_HEIGHT,
- NODE_WIDTH,
} from "./diagram_constants";
import DiagramIcon from "./DiagramIcon.vue";
@@ -417,9 +421,11 @@ const parentComponentId = computed(() => props.node.def.parentId);
const position = computed(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const r = viewStore.components[props.node.def.id]!;
- r.width = Math.max(r.width, NODE_WIDTH);
- r.height = Math.max(r.height, NODE_WIDTH);
- return r;
+
+ return {
+ x: r.x,
+ y: r.y,
+ };
});
const colors = computed(() => {
diff --git a/app/web/src/components/ModelingDiagram/ModelingDiagram.vue b/app/web/src/components/ModelingDiagram/ModelingDiagram.vue
index 5ee6a86398..3c81007fb9 100644
--- a/app/web/src/components/ModelingDiagram/ModelingDiagram.vue
+++ b/app/web/src/components/ModelingDiagram/ModelingDiagram.vue
@@ -311,6 +311,7 @@ import { useStatusStore } from "@/store/status.store";
import { useQualificationsStore } from "@/store/qualifications.store";
import { nonNullable } from "@/utils/typescriptLinter";
import { ViewId } from "@/api/sdf/dal/views";
+import { useFeatureFlagsStore } from "@/store/feature_flags.store";
import DiagramGridBackground from "./DiagramGridBackground.vue";
import {
DiagramDrawEdgeState,
@@ -370,6 +371,10 @@ import DiagramView from "./DiagramView.vue";
const LEFT_PANEL_DRAWER_WIDTH = 230;
+// SET THIS BOOLEAN TO TRUE TO ENABLE DEBUG MODE!
+// VERY HELPFUL FOR DEBUGGING ISSUES ON THE DIAGRAM!
+const enableDebugMode = false;
+
const route = useRoute();
const toast = useToast();
@@ -394,12 +399,11 @@ const emit = defineEmits<{
const componentsStore = useComponentsStore();
const viewStore = useViewsStore();
const statusStore = useStatusStore();
+const featureFlagsStore = useFeatureFlagsStore();
const modelingEventBus = componentsStore.eventBus;
const fetchDiagramReqStatus = viewStore.getRequestStatus("FETCH_VIEW");
-const enableDebugMode = false;
-
const customFontsLoaded = useCustomFontsLoaded();
let kStage: KonvaStage;
@@ -879,6 +883,17 @@ async function onKeyDown(e: KeyboardEvent) {
e.preventDefault();
renameOnDiagramByComponentId(viewStore.selectedComponentId);
}
+ if (
+ !props.readOnly &&
+ featureFlagsStore.TEMPLATE_MGMT_FUNC_GENERATION &&
+ e.key === "t" &&
+ viewStore.restorableSelectedComponents.length === 0 &&
+ viewStore.selectedComponents.length > 0 &&
+ !viewStore.selectedComponents.some((c) => c instanceof DiagramViewData)
+ ) {
+ e.preventDefault();
+ modelingEventBus.emit("templateFromSelection");
+ }
}
function onKeyUp(e: KeyboardEvent) {
diff --git a/app/web/src/components/ModelingView/DeleteSelectionModal.vue b/app/web/src/components/ModelingView/DeleteSelectionModal.vue
index ba620f54b8..8da113da52 100644
--- a/app/web/src/components/ModelingView/DeleteSelectionModal.vue
+++ b/app/web/src/components/ModelingView/DeleteSelectionModal.vue
@@ -23,7 +23,6 @@
@@ -50,7 +49,6 @@
diff --git a/app/web/src/components/ModelingView/EraseSelectionModal.vue b/app/web/src/components/ModelingView/EraseSelectionModal.vue
index bb113809a5..7443d56486 100644
--- a/app/web/src/components/ModelingView/EraseSelectionModal.vue
+++ b/app/web/src/components/ModelingView/EraseSelectionModal.vue
@@ -14,7 +14,6 @@
diff --git a/app/web/src/components/ModelingView/ModelingRightClickMenu.vue b/app/web/src/components/ModelingView/ModelingRightClickMenu.vue
index 01dd024bc1..e52f410206 100644
--- a/app/web/src/components/ModelingView/ModelingRightClickMenu.vue
+++ b/app/web/src/components/ModelingView/ModelingRightClickMenu.vue
@@ -74,7 +74,7 @@ const componentsStore = useComponentsStore();
const funcStore = useFuncStore();
const actionsStore = useActionsStore();
const viewStore = useViewsStore();
-const ffStore = useFeatureFlagsStore();
+const featureFlagsStore = useFeatureFlagsStore();
const {
selectedComponentId,
@@ -265,7 +265,7 @@ const rightClickMenuItems = computed(() => {
const items: DropdownMenuItemObjectDef[] = [];
const disabled = false;
- if (ffStore.OUTLINER_VIEWS) {
+ if (featureFlagsStore.OUTLINER_VIEWS) {
items.push({
label: "VIEWS",
header: true,
@@ -365,7 +365,7 @@ const rightClickMenuItems = computed(() => {
// management funcs for a single selected component
if (
funcStore.managementFunctionsForSelectedComponent.length > 0 &&
- ffStore.MANAGEMENT_FUNCTIONS
+ featureFlagsStore.MANAGEMENT_FUNCTIONS
) {
const submenuItems: DropdownMenuItemObjectDef[] = [];
funcStore.managementFunctionsForSelectedComponent.forEach((fn) => {
@@ -384,7 +384,7 @@ const rightClickMenuItems = computed(() => {
});
}
- // you copy, restore, delete,
+ // you copy, restore, delete, template
items.push({
label: `Copy`,
shortcut: "⌘C",
@@ -416,6 +416,18 @@ const rightClickMenuItems = computed(() => {
disabled,
});
}
+ if (
+ restorableSelectedComponents.value.length === 0 &&
+ featureFlagsStore.TEMPLATE_MGMT_FUNC_GENERATION
+ ) {
+ items.push({
+ label: `Create Template`,
+ shortcut: "T",
+ icon: "tools",
+ onSelect: triggerTemplateFromSelection,
+ disabled,
+ });
+ }
// can erase so long as you have not selected a view
if (
@@ -549,6 +561,11 @@ function triggerRestoreSelection() {
elementPos.value = null;
}
+function triggerTemplateFromSelection() {
+ modelingEventBus.emit("templateFromSelection");
+ elementPos.value = null;
+}
+
function triggerWipeFromDiagram() {
modelingEventBus.emit("eraseSelection");
elementPos.value = null;
diff --git a/app/web/src/components/ModelingView/TemplateSelectionModal.vue b/app/web/src/components/ModelingView/TemplateSelectionModal.vue
new file mode 100644
index 0000000000..8f5099635d
--- /dev/null
+++ b/app/web/src/components/ModelingView/TemplateSelectionModal.vue
@@ -0,0 +1,167 @@
+
+
+
+
+ This will create a new template function based on
+ {{
+ selectedComponents.length === 1
+ ? "this component"
+ : `the following ${selectedComponents.length} components`
+ }}. The new function will be attached to a new asset.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/web/src/components/MultiSelectDetailsPanel.vue b/app/web/src/components/MultiSelectDetailsPanel.vue
index ed41654cb8..91999b8f2d 100644
--- a/app/web/src/components/MultiSelectDetailsPanel.vue
+++ b/app/web/src/components/MultiSelectDetailsPanel.vue
@@ -21,7 +21,6 @@
diff --git a/app/web/src/components/SecretsPanel.vue b/app/web/src/components/SecretsPanel.vue
index 032b1f83f0..0c2cb730b4 100644
--- a/app/web/src/components/SecretsPanel.vue
+++ b/app/web/src/components/SecretsPanel.vue
@@ -241,7 +241,6 @@
diff --git a/app/web/src/components/Workspace/WorkspaceModelAndView.vue b/app/web/src/components/Workspace/WorkspaceModelAndView.vue
index 211abcd509..2306802803 100644
--- a/app/web/src/components/Workspace/WorkspaceModelAndView.vue
+++ b/app/web/src/components/Workspace/WorkspaceModelAndView.vue
@@ -99,6 +99,9 @@
+
@@ -137,6 +140,7 @@ import NoSelectionDetailsPanel from "../NoSelectionDetailsPanel.vue";
import ModelingRightClickMenu from "../ModelingView/ModelingRightClickMenu.vue";
import DeleteSelectionModal from "../ModelingView/DeleteSelectionModal.vue";
import RestoreSelectionModal from "../ModelingView/RestoreSelectionModal.vue";
+import TemplateSelectionModal from "../ModelingView/TemplateSelectionModal.vue";
import CommandModal from "./CommandModal.vue";
import InsetApprovalModal from "../InsetApprovalModal.vue";
diff --git a/app/web/src/store/components.store.ts b/app/web/src/store/components.store.ts
index 177494c125..4a110c8deb 100644
--- a/app/web/src/store/components.store.ts
+++ b/app/web/src/store/components.store.ts
@@ -165,6 +165,7 @@ type EventBusEvents = {
restoreSelection: void;
refreshSelectionResource: void;
eraseSelection: void;
+ templateFromSelection: void;
panToComponent: {
component: DiagramNodeData | DiagramGroupData;
center?: boolean;
@@ -1161,6 +1162,17 @@ export const useComponentsStore = (forceChangeSetId?: ChangeSetId) => {
});
},
+ async CREATE_TEMPLATE_FUNC_FROM_COMPONENTS(templateData: {
+ assetColor: string;
+ assetName: string;
+ funcName: string;
+ components: Array;
+ }) {
+ // TODO(Wendy) - this is where the end point would be called!
+ // eslint-disable-next-line no-console
+ console.log(templateData);
+ },
+
setComponentDisplayName(
component: DiagramGroupData | DiagramNodeData,
name: string,
diff --git a/app/web/src/store/feature_flags.store.ts b/app/web/src/store/feature_flags.store.ts
index dddc7bb8c0..6192f6af9b 100644
--- a/app/web/src/store/feature_flags.store.ts
+++ b/app/web/src/store/feature_flags.store.ts
@@ -18,6 +18,7 @@ const FLAG_MAPPING = {
REBAC: "rebac",
OUTLINER_VIEWS: "diagram-outline-show-views",
SLACK_WEBHOOK: "slack_webhook",
+ TEMPLATE_MGMT_FUNC_GENERATION: "template-mgmt-func-generation",
};
type FeatureFlags = keyof typeof FLAG_MAPPING;
diff --git a/lib/vue-lib/package.json b/lib/vue-lib/package.json
index 047955bcbc..3b057735b9 100644
--- a/lib/vue-lib/package.json
+++ b/lib/vue-lib/package.json
@@ -62,6 +62,7 @@
"posthog-js": "^1.57.2",
"tailwindcss-capsize": "^3.0.3",
"ulid": "^2.3.0",
+ "vanilla-picker": "^2.12.1",
"vue": "^3.5.12",
"vue-router": "^4.4.5",
"vue-safe-teleport": "^0.1.2"
diff --git a/app/web/src/components/ColorPicker.vue b/lib/vue-lib/src/design-system/forms/ColorPicker.vue
similarity index 93%
rename from app/web/src/components/ColorPicker.vue
rename to lib/vue-lib/src/design-system/forms/ColorPicker.vue
index 6a08f1de89..081c3979fa 100644
--- a/app/web/src/components/ColorPicker.vue
+++ b/lib/vue-lib/src/design-system/forms/ColorPicker.vue
@@ -7,8 +7,9 @@
:aria-required="required ?? false"
:class="
clsx(
- 'absolute z-80 h-7 px-2xs flex flex-row gap-xs items-center dark:hover:text-action-300 hover:text-action-500',
+ 'absolute h-7 px-2xs flex flex-row gap-xs items-center dark:hover:text-action-300 hover:text-action-500',
!disabled && 'cursor-pointer',
+ insideModal ? 'z-100' : 'z-80',
)
"
>
@@ -32,6 +33,7 @@ const props = defineProps<{
required?: boolean;
modelValue: string;
disabled?: boolean;
+ insideModal?: boolean;
}>();
const emit = defineEmits<{
diff --git a/lib/vue-lib/src/design-system/forms/VormInput.vue b/lib/vue-lib/src/design-system/forms/VormInput.vue
index 008fa66694..506d9090f4 100644
--- a/lib/vue-lib/src/design-system/forms/VormInput.vue
+++ b/lib/vue-lib/src/design-system/forms/VormInput.vue
@@ -67,6 +67,7 @@ you can pass in options as props too */
'bg-neutral-100 border-neutral-400',
'bg-neutral-900 border-neutral-600',
),
+ isError && 'border-destructive-600',
],
)
"
@@ -81,13 +82,13 @@ you can pass in options as props too */
}
: null
"
- class="vorm-input__input-wrap"
:class="
clsx(
'vorm-input__input-wrap',
showCautionLines
? themeClasses('bg-caution-lines-light', 'bg-caution-lines-dark')
: '',
+ compact && isError && 'border-b border-destructive-600',
)
"
>
@@ -282,11 +283,23 @@ you can pass in options as props too */
{{ instructions }}
-
();
const exitButtonRef = ref();
function fixAutoFocusElement() {
// Headless UI automatically traps focus within the modal and focuses on the first focusable element it finds.
// While focusing on an input (if there is one) feels good, focusing on an "OK" button or the close/X button
// feels a bit agressive and looks strange
- const focusedEl = document.activeElement;
+ const focusedEl = document.activeElement as HTMLElement;
+
+ if (props.noAutoFocus) {
+ focusedEl.blur();
+ noAutoFocusTrapRef.value?.classList.add("hidden");
+ }
+
if (
focusedEl?.classList.contains("modal-close-button") ||
focusedEl?.classList.contains("vbutton") ||
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f948a6c752..ad5b6cf6ed 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -837,6 +837,9 @@ importers:
ulid:
specifier: ^2.3.0
version: 2.3.0
+ vanilla-picker:
+ specifier: ^2.12.1
+ version: 2.12.1
vue:
specifier: ^3.5.12
version: 3.5.12(typescript@4.9.5)