diff --git a/app/web/src/components/ApplyChangeSetButton.vue b/app/web/src/components/ApplyChangeSetButton.vue index 6516b3687d..bf2ed5a504 100644 --- a/app/web/src/components/ApplyChangeSetButton.vue +++ b/app/web/src/components/ApplyChangeSetButton.vue @@ -1,6 +1,8 @@ @@ -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 @@ + + + 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 }} -
- {{ validationState.errorMessage }} -
-
- {{ error_set }} +
+
+ {{ validationState.errorMessage }} +
+
+ {{ error_set }} +
@@ -517,8 +530,10 @@ watch( const { theme } = useTheme(); +const isError = computed(() => validationState.isError || error_set.value); + const computedClasses = computed(() => ({ - "--error": validationState.isError || error_set.value, + "--error": isError.value, "--focused": isFocus.value, "--disabled": disabledBySelfOrParent.value, [`--type-${props.type}`]: true, @@ -901,6 +916,13 @@ defineExpose({ color: currentColor; } } + + .vorm-input__instructions, + .vorm-input__error-message { + @apply capsize text-xs; + padding-top: @vertical-gap; + padding-bottom: 4px; + } } // this class is on whatever the input is, whether its input, textarea, select, etc @@ -1016,13 +1038,6 @@ defineExpose({ margin-top: -1px; } -.vorm-input__instructions, -.vorm-input__error-message { - @apply capsize text-xs; - padding-top: @vertical-gap; - padding-bottom: 4px; -} - .vorm-input__instructions { color: var(--text-color-muted); @@ -1049,9 +1064,6 @@ defineExpose({ } } -.vorm-input__error-message { -} - .vorm-input__input-wrap { position: relative; } @@ -1153,6 +1165,14 @@ defineExpose({ --header-text-color: @colors-black; } } + + .vorm-input__instructions, + .vorm-input__error-message { + font-size: 12px; + padding-top: 2px; + padding-bottom: 2px; + color: @colors-destructive-600; + } } .vorm-input-compact > .vorm-input__label { diff --git a/lib/vue-lib/src/design-system/index.ts b/lib/vue-lib/src/design-system/index.ts index ef3179b26c..1750a98ed0 100644 --- a/lib/vue-lib/src/design-system/index.ts +++ b/lib/vue-lib/src/design-system/index.ts @@ -1,6 +1,7 @@ // ./forms export { default as VormInput } from "./forms/VormInput.vue"; export { default as VormInputOption } from "./forms/VormInputOption.vue"; +export { default as ColorPicker } from "./forms/ColorPicker.vue"; export * from "./forms/helpers/form-disabling"; export * from "./forms/helpers/form-validation"; diff --git a/lib/vue-lib/src/design-system/modals/Modal.vue b/lib/vue-lib/src/design-system/modals/Modal.vue index d7e94e0f93..18165357e9 100644 --- a/lib/vue-lib/src/design-system/modals/Modal.vue +++ b/lib/vue-lib/src/design-system/modals/Modal.vue @@ -62,6 +62,13 @@ ) " > + + +
(); 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)