diff --git a/packages/compass-data-modeling/src/components/data-modeling.tsx b/packages/compass-data-modeling/src/components/data-modeling.tsx index ff24ef0b414..f2005a16220 100644 --- a/packages/compass-data-modeling/src/components/data-modeling.tsx +++ b/packages/compass-data-modeling/src/components/data-modeling.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import { connect } from 'react-redux'; import DiagramEditor from './diagram-editor'; -import SavedDiagramsList from './saved-diagrams-list'; +import SavedDiagramsList from './list/saved-diagrams-list'; import NewDiagramFormModal from './new-diagram/new-diagram-modal'; import type { DataModelingState } from '../store/reducer'; import { Button, css, DiagramProvider } from '@mongodb-js/compass-components'; @@ -9,10 +9,12 @@ import DiagramEditorSidePanel from './drawer/diagram-editor-side-panel'; import ReselectCollectionsModal from './reselect-collections-modal'; import { useOpenWorkspace } from '@mongodb-js/compass-workspaces/provider'; import { useDataModelSavedItems } from '../provider'; +import RenameDiagramModal from './list/rename-modal'; type DataModelingProps = { showList: boolean; currentDiagramId?: string; + renameDiagramId?: string; }; const deletedDiagramContainerStyles = css({ @@ -65,6 +67,7 @@ const DataModeling: React.FunctionComponent = ({ )} + ); }; diff --git a/packages/compass-data-modeling/src/components/drawer/collection-drawer-content.tsx b/packages/compass-data-modeling/src/components/drawer/collection-drawer-content.tsx index 22172ad2642..ea27bed5fe5 100644 --- a/packages/compass-data-modeling/src/components/drawer/collection-drawer-content.tsx +++ b/packages/compass-data-modeling/src/components/drawer/collection-drawer-content.tsx @@ -22,7 +22,7 @@ import { import { useChangeOnBlur } from './use-change-on-blur'; import { RelationshipsSection } from './relationships-section'; import { getNamespaceRelationships } from '../../utils/utils'; -import { getIsNewNameValid } from './util'; +import { useNewNameValidation } from '../../utils/use-new-name-validation'; type CollectionDrawerContentProps = { namespace: string; @@ -78,16 +78,12 @@ const CollectionDrawerContent: React.FunctionComponent< const { isValid: isCollectionNameValid, errorMessage: collectionNameEditErrorMessage, - } = useMemo( - () => - getIsNewNameValid({ - newName: collectionName, - existingNames: namespaces.map((ns) => toNS(ns).collection), - currentName: toNS(namespace).collection, - entity: 'Collection', - }), - [collectionName, namespaces, namespace] - ); + } = useNewNameValidation({ + newName: collectionName, + existingNames: namespaces.map((ns) => toNS(ns).collection), + currentName: toNS(namespace).collection, + entity: 'Collection', + }); const noteInputProps = useChangeOnBlur(note, (newNote) => { onNoteChange(namespace, newNote); diff --git a/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.tsx b/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.tsx index 7f430895558..1aa91e09d81 100644 --- a/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.tsx +++ b/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.tsx @@ -17,11 +17,10 @@ import { selectCurrentModelFromState, type SelectedItems, } from '../../store/diagram'; -import { getDefaultRelationshipName } from '../../utils'; import FieldDrawerContent from './field-drawer-content'; import type { FieldPath } from '../../services/data-model-storage'; import { getFieldFromSchema } from '../../utils/schema-traversal'; -import { isIdField } from '../../utils/utils'; +import { getDefaultRelationshipName, isIdField } from '../../utils/utils'; import DiagramOverviewDrawerContent from './diagram-overview-drawer-content'; export const DATA_MODELING_DRAWER_ID = 'data-modeling-drawer'; diff --git a/packages/compass-data-modeling/src/components/drawer/diagram-overview-drawer-content.tsx b/packages/compass-data-modeling/src/components/drawer/diagram-overview-drawer-content.tsx index bee781321b7..403938ddeaa 100644 --- a/packages/compass-data-modeling/src/components/drawer/diagram-overview-drawer-content.tsx +++ b/packages/compass-data-modeling/src/components/drawer/diagram-overview-drawer-content.tsx @@ -17,9 +17,9 @@ import { DMFormFieldContainer, } from './drawer-section-components'; import { useChangeOnBlur } from './use-change-on-blur'; -import { getIsNewNameValid } from './util'; import { useConnectionInfoForId } from '@mongodb-js/compass-connections/provider'; import { useDataModelSavedItems } from '../../provider'; +import { useNewNameValidation } from '../../utils/use-new-name-validation'; const infoContainerStyles = css({ marginTop: spacing[400], @@ -115,16 +115,12 @@ const DiagramOverviewDrawerContent: React.FunctionComponent< const { isValid: isDiagramNameValid, errorMessage: diagramNameEditErrorMessage, - } = useMemo( - () => - getIsNewNameValid({ - newName: diagramName, - existingNames: diagramNames, - currentName: _diagramName, - entity: 'Diagram', - }), - [diagramName, _diagramName, diagramNames] - ); + } = useNewNameValidation({ + newName: diagramName, + existingNames: diagramNames, + currentName: _diagramName, + entity: 'Diagram', + }); return ( <> diff --git a/packages/compass-data-modeling/src/components/drawer/relationships-section.tsx b/packages/compass-data-modeling/src/components/drawer/relationships-section.tsx index 5917cae2aeb..96cb8cd87b4 100644 --- a/packages/compass-data-modeling/src/components/drawer/relationships-section.tsx +++ b/packages/compass-data-modeling/src/components/drawer/relationships-section.tsx @@ -13,8 +13,10 @@ import { useDarkMode, } from '@mongodb-js/compass-components'; import type { Relationship } from '../../services/data-model-storage'; -import { getDefaultRelationshipName } from '../../utils'; -import { isRelationshipValid } from '../../utils/utils'; +import { + getDefaultRelationshipName, + isRelationshipValid, +} from '../../utils/utils'; const titleBtnStyles = css({ marginLeft: 'auto', diff --git a/packages/compass-data-modeling/src/components/drawer/util.ts b/packages/compass-data-modeling/src/components/drawer/util.ts deleted file mode 100644 index ea8864e104b..00000000000 --- a/packages/compass-data-modeling/src/components/drawer/util.ts +++ /dev/null @@ -1,34 +0,0 @@ -export function getIsNewNameValid({ - newName, - existingNames, - currentName, - entity, -}: { - newName: string; - existingNames: string[]; - currentName: string; - entity: string; -}): { - isValid: boolean; - errorMessage?: string; -} { - if (newName.trim().length === 0) { - return { - isValid: false, - errorMessage: `${entity} name cannot be empty.`, - }; - } - - const existingNamesWithoutCurrent = existingNames.filter( - (name) => name !== currentName - ); - - const isDuplicate = existingNamesWithoutCurrent.some( - (name) => name.trim() === newName.trim() - ); - - return { - isValid: !isDuplicate, - errorMessage: isDuplicate ? `${entity} name must be unique.` : undefined, - }; -} diff --git a/packages/compass-data-modeling/src/components/diagram-card.spec.tsx b/packages/compass-data-modeling/src/components/list/diagram-card.spec.tsx similarity index 95% rename from packages/compass-data-modeling/src/components/diagram-card.spec.tsx rename to packages/compass-data-modeling/src/components/list/diagram-card.spec.tsx index 1b39b21b929..625a65e090d 100644 --- a/packages/compass-data-modeling/src/components/diagram-card.spec.tsx +++ b/packages/compass-data-modeling/src/components/list/diagram-card.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { expect } from 'chai'; import { render, screen } from '@mongodb-js/testing-library-compass'; import { DiagramCard } from './diagram-card'; -import type { Edit } from '../services/data-model-storage'; +import type { Edit } from '../../services/data-model-storage'; describe('DiagramCard', () => { const props = { diff --git a/packages/compass-data-modeling/src/components/diagram-card.tsx b/packages/compass-data-modeling/src/components/list/diagram-card.tsx similarity index 94% rename from packages/compass-data-modeling/src/components/diagram-card.tsx rename to packages/compass-data-modeling/src/components/list/diagram-card.tsx index dda15de5fd7..7f34e7556d1 100644 --- a/packages/compass-data-modeling/src/components/diagram-card.tsx +++ b/packages/compass-data-modeling/src/components/list/diagram-card.tsx @@ -10,7 +10,7 @@ import { useDarkMode, useFormattedDate, } from '@mongodb-js/compass-components'; -import type { MongoDBDataModelDescription } from '../services/data-model-storage'; +import type { MongoDBDataModelDescription } from '../../services/data-model-storage'; import React from 'react'; // Same as saved-queries-aggregations @@ -80,7 +80,7 @@ export function DiagramCard({ }: { diagram: MongoDBDataModelDescription; onOpen: (diagram: MongoDBDataModelDescription) => void; - onRename: (id: string) => void; + onRename: (id: string, name: string) => void; onDelete: (id: string) => void; }) { const darkmode = useDarkMode(); @@ -111,7 +111,7 @@ export function DiagramCard({ onAction={(action) => { switch (action) { case 'rename': - onRename(diagram.id); + onRename(diagram.id, diagram.name); break; case 'delete': onDelete(diagram.id); diff --git a/packages/compass-data-modeling/src/components/diagram-list-toolbar.tsx b/packages/compass-data-modeling/src/components/list/diagram-list-toolbar.tsx similarity index 100% rename from packages/compass-data-modeling/src/components/diagram-list-toolbar.tsx rename to packages/compass-data-modeling/src/components/list/diagram-list-toolbar.tsx diff --git a/packages/compass-data-modeling/src/components/import-diagram-button.tsx b/packages/compass-data-modeling/src/components/list/import-diagram-button.tsx similarity index 100% rename from packages/compass-data-modeling/src/components/import-diagram-button.tsx rename to packages/compass-data-modeling/src/components/list/import-diagram-button.tsx diff --git a/packages/compass-data-modeling/src/components/list/rename-modal.tsx b/packages/compass-data-modeling/src/components/list/rename-modal.tsx new file mode 100644 index 00000000000..bf76fbd9b07 --- /dev/null +++ b/packages/compass-data-modeling/src/components/list/rename-modal.tsx @@ -0,0 +1,110 @@ +import { + Button, + css, + FormFieldContainer, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + TextInput, +} from '@mongodb-js/compass-components'; +import React, { useCallback, useMemo } from 'react'; +import { useNewNameValidation } from '../../utils/use-new-name-validation'; +import { renameDiagram } from '../../store/diagram'; +import { useDataModelSavedItems } from '../../provider'; +import type { DataModelingState } from '../../store/reducer'; +import { connect } from 'react-redux'; +import { closeRenameDiagramModal } from '../../store/rename-diagram-modal'; + +const inputStyles = css({ + height: `84px`, + minHeight: `84px`, +}); + +type RenameDiagramModalProps = { + isModalOpen: boolean; + diagramId?: string; + diagramName?: string; + onRename: (id: string, newName: string) => void; + onCloseClick: () => void; +}; + +const RenameDiagramModal: React.FC = ({ + isModalOpen, + diagramId, + diagramName: _diagramName, + onRename, + onCloseClick, +}) => { + const { items: savedDiagrams } = useDataModelSavedItems(); + const diagramNames = useMemo( + () => savedDiagrams.map((diagram) => diagram.name), + [savedDiagrams] + ); + + const [diagramName, setDiagramName] = React.useState( + _diagramName ?? '' + ); + + const handleDiagramNameChange = useCallback( + (event: React.ChangeEvent) => { + setDiagramName(event.target.value); + }, + [] + ); + + const handleRename = useCallback(() => { + if (diagramId) { + onRename(diagramId, diagramName); + } + }, [onRename, diagramId, diagramName]); + + const { isValid, errorMessage } = useNewNameValidation({ + existingNames: diagramNames, + currentName: _diagramName ?? '', + newName: diagramName, + entity: 'Diagram', + }); + + return ( + { + if (!open) onCloseClick(); + }} + > + + + + + + + + + + + ); +}; + +export default connect( + (state: DataModelingState) => { + return { + isModalOpen: state.renameDiagramModal?.isOpen ?? false, + diagramId: state.renameDiagramModal?.diagramId, + diagramName: state.renameDiagramModal?.diagramName, + key: state.renameDiagramModal?.diagramId, + }; + }, + { + onRename: renameDiagram, + onCloseClick: closeRenameDiagramModal, + } +)(RenameDiagramModal); diff --git a/packages/compass-data-modeling/src/components/saved-diagrams-list.spec.tsx b/packages/compass-data-modeling/src/components/list/saved-diagrams-list.spec.tsx similarity index 72% rename from packages/compass-data-modeling/src/components/saved-diagrams-list.spec.tsx rename to packages/compass-data-modeling/src/components/list/saved-diagrams-list.spec.tsx index 79c625c8634..85185b5f602 100644 --- a/packages/compass-data-modeling/src/components/saved-diagrams-list.spec.tsx +++ b/packages/compass-data-modeling/src/components/list/saved-diagrams-list.spec.tsx @@ -1,15 +1,17 @@ import React from 'react'; import { expect } from 'chai'; +import sinon from 'sinon'; import { screen, userEvent, waitFor, } from '@mongodb-js/testing-library-compass'; import SavedDiagramsList from './saved-diagrams-list'; -import { renderWithStore } from '../../test/setup-store'; -import type { DataModelingStore } from '../../test/setup-store'; -import { DataModelStorageServiceProvider } from '../provider'; -import type { MongoDBDataModelDescription } from '../services/data-model-storage'; +import { renderWithStore } from '../../../test/setup-store'; +import type { DataModelingStore } from '../../../test/setup-store'; +import { DataModelStorageServiceProvider } from '../../provider'; +import type { MongoDBDataModelDescription } from '../../services/data-model-storage'; +import RenameModal from './rename-modal'; const storageItems: MongoDBDataModelDescription[] = [ { @@ -97,16 +99,16 @@ const storageItems: MongoDBDataModelDescription[] = [ const renderSavedDiagramsList = ({ items = storageItems, + onSave = () => Promise.resolve(false), }: { items?: MongoDBDataModelDescription[]; + onSave?: (diagram: MongoDBDataModelDescription) => Promise; } = {}) => { const mockDataModelStorage = { status: 'READY', error: null, items, - save: () => { - return Promise.resolve(false); - }, + save: onSave, delete: () => { return Promise.resolve(false); }, @@ -118,6 +120,7 @@ const renderSavedDiagramsList = ({ return renderWithStore( + , { services: { @@ -158,9 +161,13 @@ describe('SavedDiagramsList', function () { context('when there are diagrams', function () { let store: DataModelingStore; + let saveSpy: sinon.SinonSpy; beforeEach(async function () { - const result = renderSavedDiagramsList(); + saveSpy = sinon.spy(); + const result = renderSavedDiagramsList({ + onSave: saveSpy, + }); store = result.store; // wait till the list is loaded @@ -187,6 +194,55 @@ describe('SavedDiagramsList', function () { expect(store.getState().generateDiagramWizard.inProgress).to.be.true; }); + it('allows renaming a diagram', async function () { + const actionsBtns = screen.getAllByRole('button', { + name: 'Show actions', + }); + expect(actionsBtns.length).to.equal(3); + + // Open actions menu for the first diagram + userEvent.click(actionsBtns[0]); + + // Click Rename + const renameBtn = screen.getByRole('menuitem', { name: 'Rename' }); + expect(renameBtn).to.be.visible; + userEvent.click(renameBtn); + + // Wait for the modal to open + await waitFor(() => { + expect(screen.getByText('Rename diagram')).to.be.visible; + }); + + const nameInput = screen.getByLabelText('Name'); + const submitButton = screen.getByRole('button', { name: 'Rename' }); + expect(nameInput).to.have.attribute('aria-invalid', 'false'); + + // Empty is disabled + userEvent.clear(nameInput); + expect(nameInput).to.have.attribute('aria-invalid', 'true'); + expect(submitButton).to.have.attribute('aria-disabled', 'true'); + + // Duplicate is disabled + userEvent.type(nameInput, 'Two'); + expect(nameInput).to.have.attribute('aria-invalid', 'true'); + expect(submitButton).to.have.attribute('aria-disabled', 'true'); + + // Valid name enables the Rename button + userEvent.clear(nameInput); + userEvent.type(nameInput, 'New Name'); + expect(nameInput).to.have.attribute('aria-invalid', 'false'); + expect(submitButton).to.have.attribute('aria-disabled', 'false'); + userEvent.click(submitButton); + + // Wait for the modal to close + await waitFor(() => { + expect(screen.queryByText('Rename diagram')).to.not.exist; + expect(saveSpy).to.have.been.calledOnceWith( + sinon.match.has('name', 'New Name') + ); + }); + }); + describe('search', function () { it('filters the list of diagrams by name', async function () { const searchInput = screen.getByPlaceholderText('Search'); diff --git a/packages/compass-data-modeling/src/components/saved-diagrams-list.tsx b/packages/compass-data-modeling/src/components/list/saved-diagrams-list.tsx similarity index 92% rename from packages/compass-data-modeling/src/components/saved-diagrams-list.tsx rename to packages/compass-data-modeling/src/components/list/saved-diagrams-list.tsx index 6306ada7858..ee84f2d2199 100644 --- a/packages/compass-data-modeling/src/components/saved-diagrams-list.tsx +++ b/packages/compass-data-modeling/src/components/list/saved-diagrams-list.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from 'react'; import { connect } from 'react-redux'; -import { createNewDiagram } from '../store/generate-diagram-wizard'; +import { createNewDiagram } from '../../store/generate-diagram-wizard'; import { Button, css, @@ -13,20 +13,20 @@ import { WorkspaceContainer, Body, } from '@mongodb-js/compass-components'; -import { useDataModelSavedItems } from '../provider'; +import { useDataModelSavedItems } from '../../provider'; import { deleteDiagram, openDiagram, openDiagramFromFile, - showDiagramRenameModal, -} from '../store/diagram'; -import type { MongoDBDataModelDescription } from '../services/data-model-storage'; -import CollaborateIcon from './icons/collaborate'; -import SchemaVisualizationIcon from './icons/schema-visualization'; -import FlexibilityIcon from './icons/flexibility'; +} from '../../store/diagram'; +import type { MongoDBDataModelDescription } from '../../services/data-model-storage'; +import CollaborateIcon from '../icons/collaborate'; +import SchemaVisualizationIcon from '../icons/schema-visualization'; +import FlexibilityIcon from '../icons/flexibility'; import { CARD_HEIGHT, CARD_WIDTH, DiagramCard } from './diagram-card'; import { DiagramListToolbar } from './diagram-list-toolbar'; import { ImportDiagramButton } from './import-diagram-button'; +import { openRenameDiagramModal } from '../../store/rename-diagram-modal'; const sortBy = [ { @@ -183,7 +183,7 @@ export const SavedDiagramsList: React.FunctionComponent<{ onCreateDiagramClick: () => void; onOpenDiagramClick: (diagram: MongoDBDataModelDescription) => void; onDiagramDeleteClick: (id: string) => void; - onDiagramRenameClick: (id: string) => void; + onDiagramRenameClick: (id: string, name: string) => void; onImportDiagramClick: (file: File) => void; }> = ({ onCreateDiagramClick, @@ -263,6 +263,6 @@ export default connect(null, { onCreateDiagramClick: createNewDiagram, onOpenDiagramClick: openDiagram, onDiagramDeleteClick: deleteDiagram, - onDiagramRenameClick: showDiagramRenameModal, + onDiagramRenameClick: openRenameDiagramModal, onImportDiagramClick: openDiagramFromFile, })(SavedDiagramsList); diff --git a/packages/compass-data-modeling/src/store/diagram.ts b/packages/compass-data-modeling/src/store/diagram.ts index 4ae9eb66994..1edb20b16f2 100644 --- a/packages/compass-data-modeling/src/store/diagram.ts +++ b/packages/compass-data-modeling/src/store/diagram.ts @@ -21,7 +21,6 @@ import { getCoordinatesForNewNode, type openToast as _openToast, showConfirmation, - showPrompt, } from '@mongodb-js/compass-components'; import { getDiagramContentsFromFile, @@ -321,6 +320,7 @@ export const diagramReducer: Reducer = ( selectedItems: null, }; } + return state; }; @@ -707,31 +707,20 @@ export function deleteDiagram( export function renameDiagram( id: string, newName: string -): RenameDiagramAction { - return { type: DiagramActionTypes.RENAME_DIAGRAM, id, name: newName }; -} - -export function showDiagramRenameModal( - id: string // TODO maybe pass the whole thing here, we always have it when calling this, then we don't need to re-load storage ): DataModelingThunkAction, RenameDiagramAction> { - return async (dispatch, getState, { dataModelStorage }) => { + return async (dispatch, getState, { dataModelStorage, openToast }) => { try { const diagram = await dataModelStorage.load(id); - if (!diagram) { - return; - } - const newName = await showPrompt({ - title: 'Rename diagram', - label: 'Name', - defaultValue: diagram.name, - }); - if (!newName) { + if (!diagram || !newName) { return; } + + await dataModelStorage.save({ ...diagram, name: newName }); dispatch({ type: DiagramActionTypes.RENAME_DIAGRAM, id, name: newName }); - void dataModelStorage.save({ ...diagram, name: newName }); - } catch { - // TODO log + } catch (error) { + handleError(openToast, 'Error renaming diagram', [ + (error as Error).message, + ]); } }; } diff --git a/packages/compass-data-modeling/src/store/reducer.ts b/packages/compass-data-modeling/src/store/reducer.ts index 64bd7776997..0c3a016e031 100644 --- a/packages/compass-data-modeling/src/store/reducer.ts +++ b/packages/compass-data-modeling/src/store/reducer.ts @@ -26,6 +26,11 @@ import type { ReselectCollectionsWizardActionTypes, } from './reselect-collections-wizard'; import { reselectCollectionsWizardReducer } from './reselect-collections-wizard'; +import { + renameDiagramModalReducer, + type RenameDiagramModalActions, + type RenameDiagramModalActionTypes, +} from './rename-diagram-modal'; const reducer = combineReducers({ step: stepReducer, @@ -33,6 +38,7 @@ const reducer = combineReducers({ analysisProgress: analysisProcessReducer, diagram: diagramReducer, exportDiagram: exportDiagramReducer, + renameDiagramModal: renameDiagramModalReducer, reselectCollections: reselectCollectionsWizardReducer, }); @@ -41,12 +47,14 @@ export type DataModelingActions = | AnalysisProgressActions | DiagramActions | ExportDiagramActions + | RenameDiagramModalActions | ReselectCollectionsWizardActions; type _ActionTypes = typeof GenerateDiagramWizardActionTypes & typeof AnalysisProcessActionTypes & typeof DiagramActionTypes & typeof ExportDiagramActionTypes & + typeof RenameDiagramModalActionTypes & typeof ReselectCollectionsWizardActionTypes; export type DataModelingActionTypes = _ActionTypes[keyof _ActionTypes]; diff --git a/packages/compass-data-modeling/src/store/rename-diagram-modal.ts b/packages/compass-data-modeling/src/store/rename-diagram-modal.ts new file mode 100644 index 00000000000..c01bf29d74a --- /dev/null +++ b/packages/compass-data-modeling/src/store/rename-diagram-modal.ts @@ -0,0 +1,84 @@ +import type { Reducer } from 'redux'; +import { isAction } from './util'; +import { DiagramActionTypes } from './diagram'; + +export type RenameDiagramModalState = { + isOpen: boolean; + diagramId?: string; + diagramName?: string; +}; + +export const RenameDiagramModalActionTypes = { + OPEN_RENAME_DIAGRAM_MODAL: + 'data-modeling/rename-diagram/OPEN_RENAME_DIAGRAM_MODAL', + CLOSE_RENAME_DIAGRAM_MODAL: + 'data-modeling/rename-diagram/CLOSE_RENAME_DIAGRAM_MODAL', +} as const; + +type ModalOpenedAction = { + type: typeof RenameDiagramModalActionTypes.OPEN_RENAME_DIAGRAM_MODAL; + id: string; + name: string; +}; + +type ModalClosedAction = { + type: typeof RenameDiagramModalActionTypes.CLOSE_RENAME_DIAGRAM_MODAL; +}; + +export type OpenRenameDiagramModalAction = { + type: typeof RenameDiagramModalActionTypes.OPEN_RENAME_DIAGRAM_MODAL; + id: string; + name: string; +}; + +export type CloseRenameDiagramModalAction = { + type: typeof RenameDiagramModalActionTypes.CLOSE_RENAME_DIAGRAM_MODAL; +}; + +export type RenameDiagramModalActions = ModalOpenedAction | ModalClosedAction; + +const INITIAL_STATE = { + isOpen: false, + diagramId: undefined, +}; + +export const renameDiagramModalReducer: Reducer = ( + state = INITIAL_STATE, + action +) => { + if ( + isAction(action, RenameDiagramModalActionTypes.OPEN_RENAME_DIAGRAM_MODAL) + ) { + return { + ...state, + isOpen: true, + diagramId: action.id, + diagramName: action.name, + }; + } + if ( + isAction( + action, + RenameDiagramModalActionTypes.CLOSE_RENAME_DIAGRAM_MODAL + ) || + isAction(action, DiagramActionTypes.RENAME_DIAGRAM) + ) { + return INITIAL_STATE; + } + return state; +}; + +export function openRenameDiagramModal( + id: string, + name: string +): OpenRenameDiagramModalAction { + return { + type: RenameDiagramModalActionTypes.OPEN_RENAME_DIAGRAM_MODAL, + id, + name, + }; +} + +export function closeRenameDiagramModal() { + return { type: RenameDiagramModalActionTypes.CLOSE_RENAME_DIAGRAM_MODAL }; +} diff --git a/packages/compass-data-modeling/src/utils.ts b/packages/compass-data-modeling/src/utils.ts deleted file mode 100644 index 8a7a933422a..00000000000 --- a/packages/compass-data-modeling/src/utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import toNS from 'mongodb-ns'; -import type { Relationship } from './services/data-model-storage'; - -export function getDefaultRelationshipName( - relationship: Relationship['relationship'] -): string { - const [local, foreign] = relationship; - let localLabel = ''; - let foreignLabel = ''; - if (local.ns) { - localLabel += toNS(local.ns).collection; - if (local.fields && local.fields.length) { - localLabel += `.${local.fields.join('.')}`; - } - } - if (foreign.ns) { - foreignLabel += toNS(foreign.ns).collection; - if (foreign.fields && foreign.fields.length) { - foreignLabel += `.${foreign.fields.join('.')}`; - } - } - return [localLabel, foreignLabel].join(` \u2192 `).trim(); -} diff --git a/packages/compass-data-modeling/src/utils/use-new-name-validation.ts b/packages/compass-data-modeling/src/utils/use-new-name-validation.ts new file mode 100644 index 00000000000..ea83e5b1bd7 --- /dev/null +++ b/packages/compass-data-modeling/src/utils/use-new-name-validation.ts @@ -0,0 +1,38 @@ +import { useMemo } from 'react'; + +export function useNewNameValidation({ + newName, + existingNames, + currentName, + entity, +}: { + newName: string; + existingNames: string[]; + currentName: string; + entity: string; +}): { + isValid: boolean; + errorMessage?: string; +} { + return useMemo(() => { + if (newName.trim().length === 0) { + return { + isValid: false, + errorMessage: `${entity} name cannot be empty.`, + }; + } + + const existingNamesWithoutCurrent = existingNames.filter( + (name) => name !== currentName + ); + + const isDuplicate = existingNamesWithoutCurrent.some( + (name) => name.trim() === newName.trim() + ); + + return { + isValid: !isDuplicate, + errorMessage: isDuplicate ? `${entity} name must be unique.` : undefined, + }; + }, [newName, existingNames, currentName, entity]); +} diff --git a/packages/compass-data-modeling/src/utils/utils.ts b/packages/compass-data-modeling/src/utils/utils.ts index 12f0329d3d3..56071cebecb 100644 --- a/packages/compass-data-modeling/src/utils/utils.ts +++ b/packages/compass-data-modeling/src/utils/utils.ts @@ -5,6 +5,7 @@ import type { Relationship, } from '../services/data-model-storage'; import { cloneDeepWith } from 'lodash'; +import toNS from 'mongodb-ns'; export const isIdField = (fieldPath: FieldPath): boolean => fieldPath.length === 1 && fieldPath[0] === '_id'; @@ -143,3 +144,24 @@ export function mapFieldDataToJsonSchema( }); return newFieldData; } + +export function getDefaultRelationshipName( + relationship: Relationship['relationship'] +): string { + const [local, foreign] = relationship; + let localLabel = ''; + let foreignLabel = ''; + if (local.ns) { + localLabel += toNS(local.ns).collection; + if (local.fields && local.fields.length) { + localLabel += `.${local.fields.join('.')}`; + } + } + if (foreign.ns) { + foreignLabel += toNS(foreign.ns).collection; + if (foreign.fields && foreign.fields.length) { + foreignLabel += `.${foreign.fields.join('.')}`; + } + } + return [localLabel, foreignLabel].join(` \u2192 `).trim(); +}