diff --git a/.changeset/duplicate-to-drafts-conditional.md b/.changeset/duplicate-to-drafts-conditional.md new file mode 100644 index 000000000..c77e7342b --- /dev/null +++ b/.changeset/duplicate-to-drafts-conditional.md @@ -0,0 +1,5 @@ +--- +'@workflowbuilder/sdk': patch +--- + +The app bar's "Duplicate to Drafts" menu item now renders only when an `onDuplicateClick` handler is provided, removing the default no-op button. diff --git a/.changeset/template-selector-title.md b/.changeset/template-selector-title.md new file mode 100644 index 000000000..05bf1a61e --- /dev/null +++ b/.changeset/template-selector-title.md @@ -0,0 +1,5 @@ +--- +'@workflowbuilder/sdk': patch +--- + +The template selector modal title now uses the SDK's own `templateSelector.title` string instead of an unrelated plugin translation key. diff --git a/apps/demo/src/app/plugins/help/README.md b/apps/demo/src/app/plugins/help/README.md index de65348a0..0adf94dd0 100644 --- a/apps/demo/src/app/plugins/help/README.md +++ b/apps/demo/src/app/plugins/help/README.md @@ -1,6 +1,6 @@ # Help -Removal of this plugin will result in an application without the "_Unlock Full Product Access_" popup and watermark in the corner. +Removal of this plugin will result in an application without the watermark in the corner. ## Plugins in Workflow Builder diff --git a/apps/demo/src/app/plugins/help/assets/maciej-teska-workflow.jpg b/apps/demo/src/app/plugins/help/assets/maciej-teska-workflow.jpg deleted file mode 100644 index 17fc75993..000000000 Binary files a/apps/demo/src/app/plugins/help/assets/maciej-teska-workflow.jpg and /dev/null differ diff --git a/apps/demo/src/app/plugins/help/assets/watermark.svg b/apps/demo/src/app/plugins/help/assets/watermark.svg index 478148638..441b3aa79 100644 --- a/apps/demo/src/app/plugins/help/assets/watermark.svg +++ b/apps/demo/src/app/plugins/help/assets/watermark.svg @@ -1,30 +1,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/apps/demo/src/app/plugins/help/components/app-bar/get-app-bar-button.tsx b/apps/demo/src/app/plugins/help/components/app-bar/get-app-bar-button.tsx deleted file mode 100644 index f946a21f0..000000000 --- a/apps/demo/src/app/plugins/help/components/app-bar/get-app-bar-button.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { NavButton } from '@synergycodes/overflow-ui'; -import { Icon } from '@workflowbuilder/sdk'; -import type { TranslationKey, WBIcon } from '@workflowbuilder/sdk'; -import i18n from 'i18next'; - -import { openNoAccessModal } from '../../functions/open-no-access-modal'; - -export function getAppBarButton(icon: WBIcon, tooltip?: TranslationKey) { - function mockAppBarButton() { - return ( - - - - ); - } - - Object.defineProperty(mockAppBarButton, 'name', { value: `mockAppBarButton` }); - - return mockAppBarButton; -} diff --git a/apps/demo/src/app/plugins/help/components/footer-support-button.tsx b/apps/demo/src/app/plugins/help/components/footer-support-button.tsx deleted file mode 100644 index 8acf4a4c6..000000000 --- a/apps/demo/src/app/plugins/help/components/footer-support-button.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Button } from '@synergycodes/overflow-ui'; -import { useTranslation } from 'react-i18next'; - -import { openHelpModal } from '../functions/open-help-modal'; - -export function FooterSupportButton() { - const { t } = useTranslation(); - - return ( - - ); -} diff --git a/apps/demo/src/app/plugins/help/functions/add-items-to-dots.tsx b/apps/demo/src/app/plugins/help/functions/add-items-to-dots.tsx deleted file mode 100644 index 35096a5fb..000000000 --- a/apps/demo/src/app/plugins/help/functions/add-items-to-dots.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { type MenuItemProps } from '@synergycodes/overflow-ui'; -import { Icon } from '@workflowbuilder/sdk'; -import i18n from 'i18next'; - -import { openNoAccessModal } from './open-no-access-modal'; - -export function addItemsToDots({ returnValue }: { returnValue: unknown }) { - if (!Array.isArray(returnValue)) { - return; - } - - const items = returnValue as MenuItemProps[]; - - const newItems: MenuItemProps[] = [ - { - label: i18n.t('header.controls.saveAsImage'), - icon: , - onClick: openNoAccessModal, - }, - { - type: 'separator', - }, - { - label: i18n.t('header.controls.archive'), - icon: , - destructive: true, - onClick: openNoAccessModal, - }, - ]; - - return { replacedReturn: [...items, ...newItems] }; -} diff --git a/apps/demo/src/app/plugins/help/functions/open-help-modal.tsx b/apps/demo/src/app/plugins/help/functions/open-help-modal.tsx deleted file mode 100644 index ce76640c7..000000000 --- a/apps/demo/src/app/plugins/help/functions/open-help-modal.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Info } from '@phosphor-icons/react'; -import { openModal } from '@workflowbuilder/sdk'; -import i18n from 'i18next'; - -import { SalesContact } from '../modals/sales-contact/sales-contact'; - -export function openHelpModal() { - openModal({ - content: , - icon: , - title: i18n.t('plugins.help.helpSupport'), - }); -} diff --git a/apps/demo/src/app/plugins/help/functions/open-no-access-modal.tsx b/apps/demo/src/app/plugins/help/functions/open-no-access-modal.tsx deleted file mode 100644 index 94e58146a..000000000 --- a/apps/demo/src/app/plugins/help/functions/open-no-access-modal.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Icon, openModal } from '@workflowbuilder/sdk'; -import i18n from 'i18next'; - -import { NoAccess } from '../modals/no-access/no-access'; -import { SalesContact } from '../modals/sales-contact/sales-contact'; - -export function openNoAccessModal() { - openModal({ - content: , - footer: , - footerVariant: 'separated', - icon: , - title: i18n.t('plugins.help.header'), - }); -} diff --git a/apps/demo/src/app/plugins/help/locales/en/translation.json b/apps/demo/src/app/plugins/help/locales/en/translation.json deleted file mode 100644 index 9cc572475..000000000 --- a/apps/demo/src/app/plugins/help/locales/en/translation.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "plugins": { - "help": { - "helpSupport": "Help & Support", - "header": "Unlock Full Product Access", - "title": "Reach out to learn more", - "subtitle": "Connect with us for full access details.", - "tooltipElk": "Refresh layout", - "tooltipUndo": "Undo", - "tooltipRedo": "Redo", - "tooltipOpen": "Open" - } - } -} diff --git a/apps/demo/src/app/plugins/help/locales/pl/translation.json b/apps/demo/src/app/plugins/help/locales/pl/translation.json deleted file mode 100644 index 426d35a16..000000000 --- a/apps/demo/src/app/plugins/help/locales/pl/translation.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "plugins": { - "help": { - "helpSupport": "Pomoc i wsparcie", - "header": "Otrzymaj pełny dostęp do produktu", - "title": "Skontaktuj się, aby dowiedzieć się więcej", - "subtitle": "Uzyskaj szczegółowe informacje o pełnym dostępie.", - "tooltipElk": "Odśwież layout", - "tooltipUndo": "Cofnij", - "tooltipRedo": "Ponów", - "tooltipOpen": "Otwórz" - } - } -} diff --git a/apps/demo/src/app/plugins/help/modals/no-access/no-access.module.css b/apps/demo/src/app/plugins/help/modals/no-access/no-access.module.css deleted file mode 100644 index b88c019ef..000000000 --- a/apps/demo/src/app/plugins/help/modals/no-access/no-access.module.css +++ /dev/null @@ -1,16 +0,0 @@ -:root { - --wb-no-access-color: var(--ax-txt-primary-default); - --wb-no-access-sub-title-color: var(--ax-txt-secondary-default); -} - -.container { - width: 100%; - display: flex; - flex-direction: column; - gap: 0.5rem; - color: var(--wb-no-access-color); - - .sub-title { - color: var(--wb-no-access-sub-title-color); - } -} diff --git a/apps/demo/src/app/plugins/help/modals/no-access/no-access.tsx b/apps/demo/src/app/plugins/help/modals/no-access/no-access.tsx deleted file mode 100644 index 1d2cdd8f5..000000000 --- a/apps/demo/src/app/plugins/help/modals/no-access/no-access.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import clsx from 'clsx'; -import { useTranslation } from 'react-i18next'; - -import styles from './no-access.module.css'; - -export function NoAccess() { - const { t } = useTranslation(); - - return ( -
- {t('plugins.help.title')} - {t('plugins.help.subtitle')} -
- ); -} diff --git a/apps/demo/src/app/plugins/help/modals/sales-contact/sales-contact.module.css b/apps/demo/src/app/plugins/help/modals/sales-contact/sales-contact.module.css deleted file mode 100644 index 5789f3621..000000000 --- a/apps/demo/src/app/plugins/help/modals/sales-contact/sales-contact.module.css +++ /dev/null @@ -1,32 +0,0 @@ -:root { - --wb-sales-contact-color: var(--ax-txt-primary-default); - --wb-sales-contact-position-color: var(--ax-txt-secondary-default); -} - -.container { - width: 100%; - display: flex; - justify-content: space-between; - gap: 0.25rem; - - .details-container { - display: flex; - align-items: center; - gap: 0.5rem; - - .details { - display: flex; - flex-direction: column; - color: var(--wb-sales-contact-color); - - .position { - color: var(--wb-sales-contact-position-color); - } - } - } - - .buttons { - display: flex; - gap: 0.625rem; - } -} diff --git a/apps/demo/src/app/plugins/help/modals/sales-contact/sales-contact.tsx b/apps/demo/src/app/plugins/help/modals/sales-contact/sales-contact.tsx deleted file mode 100644 index 1c778f81e..000000000 --- a/apps/demo/src/app/plugins/help/modals/sales-contact/sales-contact.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { LinkedinLogo, PaperPlaneRight } from '@phosphor-icons/react'; -import { Avatar, Button } from '@synergycodes/overflow-ui'; -import clsx from 'clsx'; - -import styles from './sales-contact.module.css'; - -import imageUrl from '../../assets/maciej-teska-workflow.jpg'; - -const salesDetails = { - name: 'Maciej Teska', - imageUrl, - position: 'CEO', - email: 'maciej.teska@workflowbuilder.io', - linkedInUrl: 'https://linkedin.com/in/maciej-teska', -}; - -export function SalesContact() { - const { name, position, imageUrl, email, linkedInUrl } = salesDetails; - function handleLinkedInClick() { - window.open(`${linkedInUrl}`, '_blank'); - } - - function handleEmailClick() { - globalThis.location.href = `mailto:${email}`; - } - - return ( -
-
- -
- {name} - {position} -
-
-
- - -
-
- ); -} diff --git a/apps/demo/src/app/plugins/help/plugin-exports.ts b/apps/demo/src/app/plugins/help/plugin-exports.ts index a48f9dd80..0026430e5 100644 --- a/apps/demo/src/app/plugins/help/plugin-exports.ts +++ b/apps/demo/src/app/plugins/help/plugin-exports.ts @@ -1,86 +1,10 @@ -import { - hasRegisteredComponentDecorator, - registerComponentDecorator, - registerFunctionDecorator, - registerPluginTranslation, -} from '@workflowbuilder/sdk'; -import type { DiagramContainerProps, ProjectSelectionProps, PropertiesBarProps } from '@workflowbuilder/sdk'; +import { registerComponentDecorator } from '@workflowbuilder/sdk'; +import type { DiagramContainerProps } from '@workflowbuilder/sdk'; -import { getAppBarButton } from './components/app-bar/get-app-bar-button'; -import { FooterSupportButton } from './components/footer-support-button'; import { Watermark } from './components/watermark/watermark'; -import { addItemsToDots } from './functions/add-items-to-dots'; -import { openNoAccessModal } from './functions/open-no-access-modal'; -import * as translationEN from './locales/en/translation.json'; -import * as translationPL from './locales/pl/translation.json'; export function plugin(): void { - registerComponentDecorator('OptionalFooterContent', { - content: FooterSupportButton, - place: 'after', - }); - registerComponentDecorator('DiagramContainer', { content: Watermark, }); - - registerFunctionDecorator('getControlsDotsItems', { - callback: addItemsToDots, - place: 'after', - priority: 10, - }); - - registerComponentDecorator('ProjectSelection', { - modifyProps: (props) => ({ - ...props, - onDuplicateClick: openNoAccessModal, - }), - }); - - registerComponentDecorator('PropertiesBar', { - modifyProps: (props) => ({ - ...props, - onMenuHeaderClick: openNoAccessModal, - }), - }); - - registerComponentDecorator('OptionalAppBarTools', { - content: getAppBarButton('FolderOpen', 'plugins.help.tooltipOpen'), - place: 'after', - priority: 10, - }); - - /* - This plugin checks whether those buttons are already registered - to avoid rendering hints about features that have been added to the project. - */ - if (hasRegisteredComponentDecorator('OptionalAppBarTools', 'UndoRedo') === false) { - registerComponentDecorator('OptionalAppBarTools', { - content: getAppBarButton('ArrowUUpLeft', 'plugins.help.tooltipUndo'), - place: 'after', - name: 'OptionalAppBarToolsArrowUUpLeft', - }); - - registerComponentDecorator('OptionalAppBarTools', { - content: getAppBarButton('ArrowUUpRight', 'plugins.help.tooltipRedo'), - place: 'after', - name: 'OptionalAppBarToolsArrowUUpRight', - }); - } - - if (hasRegisteredComponentDecorator('OptionalAppBarControls', 'ElkLayout') === false) { - registerComponentDecorator('OptionalAppBarControls', { - content: getAppBarButton('TreeStructureDown', 'plugins.help.tooltipElk'), - place: 'before', - }); - } - - registerPluginTranslation({ - en: { - translation: translationEN, - }, - pl: { - translation: translationPL, - }, - }); } diff --git a/packages/sdk/src/features/app-bar/app-bar-container.tsx b/packages/sdk/src/features/app-bar/app-bar-container.tsx index c0c1782e0..b6236cf39 100644 --- a/packages/sdk/src/features/app-bar/app-bar-container.tsx +++ b/packages/sdk/src/features/app-bar/app-bar-container.tsx @@ -1,7 +1,6 @@ import styles from './app-bar.module.css'; import './variables.css'; -import { noop } from '../../utils/noop'; import { Controls } from './components/controls/controls'; import { ProjectSelection } from './components/project-selection/project-selection'; import { Toolbar } from './components/toolbar/toolbar'; @@ -17,7 +16,7 @@ export function AppBarContainer() { return (
- +
); diff --git a/packages/sdk/src/features/app-bar/components/project-selection/project-selection.spec.tsx b/packages/sdk/src/features/app-bar/components/project-selection/project-selection.spec.tsx new file mode 100644 index 000000000..a75151c66 --- /dev/null +++ b/packages/sdk/src/features/app-bar/components/project-selection/project-selection.spec.tsx @@ -0,0 +1,63 @@ +import type { MenuItemProps } from '@synergycodes/overflow-ui'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +// Render the menu's `items` inline so we can assert on them without driving the +// real overflow-ui popover. The component also imports Input/NavButton, so the +// mock must expose them too. +vi.mock('@synergycodes/overflow-ui', () => ({ + Menu: ({ items }: { items: MenuItemProps[] }) => ( +
    + {items.map((item) => ( +
  • + +
  • + ))} +
+ ), + NavButton: () => null, + Input: () => null, +})); + +vi.mock('@workflow-builder/icons', () => ({ + Icon: () => null, +})); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +vi.mock('../../../../features/variables/modals/modal-settings', () => ({ + openModalWorkflowSettings: vi.fn(), +})); + +vi.mock('../../../../store/store', () => ({ + useStore: ( + selector: (state: { documentName: string; isReadOnlyMode: boolean; setDocumentName: () => void }) => T, + ) => selector({ documentName: 'Doc', isReadOnlyMode: false, setDocumentName: () => {} }), +})); + +const { ProjectSelection } = await import('./project-selection'); + +const DUPLICATE_LABEL = 'header.projectSelection.duplicateToDrafts'; +const SETTINGS_LABEL = 'common.settings'; + +describe('ProjectSelection — "Duplicate to Drafts" visibility', () => { + it('omits the item when no onDuplicateClick is provided (default editor)', () => { + render(); + + expect(screen.getByText(SETTINGS_LABEL)).toBeDefined(); + expect(screen.queryByText(DUPLICATE_LABEL)).toBeNull(); + }); + + it('renders the item and wires the handler when onDuplicateClick is provided', () => { + const onDuplicateClick = vi.fn(); + render(); + + const item = screen.getByText(DUPLICATE_LABEL); + expect(item).toBeDefined(); + + fireEvent.click(item); + expect(onDuplicateClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/sdk/src/features/app-bar/components/project-selection/project-selection.tsx b/packages/sdk/src/features/app-bar/components/project-selection/project-selection.tsx index 168de52a7..6ca66b438 100644 --- a/packages/sdk/src/features/app-bar/components/project-selection/project-selection.tsx +++ b/packages/sdk/src/features/app-bar/components/project-selection/project-selection.tsx @@ -19,7 +19,10 @@ import { withOptionalComponentPlugins } from '../../../plugins-core/adapters/ada * @category Components */ export type ProjectSelectionProps = { - /** Optional handler wired into the kebab menu's "Duplicate to drafts" item. */ + /** + * Optional handler for the kebab menu's "Duplicate to Drafts" item. The item + * is rendered only when this is provided — omit it and the item is absent. + */ onDuplicateClick?: () => void; }; @@ -46,11 +49,17 @@ function ProjectSelectionComponent({ onDuplicateClick }: ProjectSelectionProps) icon: , onClick: openModalWorkflowSettings, }, - { - label: t('header.projectSelection.duplicateToDrafts'), - icon: , - onClick: onDuplicateClick, - }, + // Rendered only when the host wires a handler; without one it would be a + // dead no-op item, so we leave it out entirely. + ...(onDuplicateClick + ? [ + { + label: t('header.projectSelection.duplicateToDrafts'), + icon: , + onClick: onDuplicateClick, + }, + ] + : []), ], [onDuplicateClick, t], ); diff --git a/packages/sdk/src/features/modals/template-selector/open-template-selector-modal.tsx b/packages/sdk/src/features/modals/template-selector/open-template-selector-modal.tsx index e66c173f5..4a6b45475 100644 --- a/packages/sdk/src/features/modals/template-selector/open-template-selector-modal.tsx +++ b/packages/sdk/src/features/modals/template-selector/open-template-selector-modal.tsx @@ -10,7 +10,7 @@ export function openTemplateSelectorModal() { openModal({ content: , icon: , - title: i18n.t('plugins.help.header'), + title: i18n.t('templateSelector.title'), onModalClosed: () => useStore.getState().setDiagramModel(undefined, { skipIfNotEmpty: true }), }); }