mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-13 01:38:35 +00:00
feat(homePage): add the ability to create asset collection module (#14050)
Co-authored-by: Chris Collins <chriscollins3456@gmail.com>
This commit is contained in:
parent
b550b8a903
commit
1350fe21fa
@ -2,6 +2,7 @@ package com.linkedin.datahub.graphql.resolvers.module;
|
|||||||
|
|
||||||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
|
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
|
||||||
|
|
||||||
|
import com.linkedin.common.UrnArray;
|
||||||
import com.linkedin.common.urn.Urn;
|
import com.linkedin.common.urn.Urn;
|
||||||
import com.linkedin.common.urn.UrnUtils;
|
import com.linkedin.common.urn.UrnUtils;
|
||||||
import com.linkedin.datahub.graphql.QueryContext;
|
import com.linkedin.datahub.graphql.QueryContext;
|
||||||
@ -16,7 +17,9 @@ import com.linkedin.metadata.service.PageModuleService;
|
|||||||
import com.linkedin.module.DataHubPageModuleParams;
|
import com.linkedin.module.DataHubPageModuleParams;
|
||||||
import graphql.schema.DataFetcher;
|
import graphql.schema.DataFetcher;
|
||||||
import graphql.schema.DataFetchingEnvironment;
|
import graphql.schema.DataFetchingEnvironment;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -91,6 +94,20 @@ public class UpsertPageModuleResolver implements DataFetcher<CompletableFuture<D
|
|||||||
gmsParams.setRichTextParams(richTextParams);
|
gmsParams.setRichTextParams(richTextParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (paramsInput.getAssetCollectionParams() != null) {
|
||||||
|
com.linkedin.module.AssetCollectionModuleParams assetCollectionParams =
|
||||||
|
new com.linkedin.module.AssetCollectionModuleParams();
|
||||||
|
|
||||||
|
List<Urn> urns =
|
||||||
|
paramsInput.getAssetCollectionParams().getAssetUrns().stream()
|
||||||
|
.map(UrnUtils::getUrn)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
UrnArray urnArray = new UrnArray(urns);
|
||||||
|
|
||||||
|
assetCollectionParams.setAssetUrns(urnArray);
|
||||||
|
gmsParams.setAssetCollectionParams(assetCollectionParams);
|
||||||
|
}
|
||||||
return gmsParams;
|
return gmsParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +123,11 @@ public class UpsertPageModuleResolver implements DataFetcher<CompletableFuture<D
|
|||||||
if (params.getLinkParams() == null) {
|
if (params.getLinkParams() == null) {
|
||||||
throw new IllegalArgumentException("Did not provide link params for link module");
|
throw new IllegalArgumentException("Did not provide link params for link module");
|
||||||
}
|
}
|
||||||
|
} else if (type.equals(com.linkedin.module.DataHubPageModuleType.ASSET_COLLECTION)) {
|
||||||
|
if (params.getAssetCollectionParams() == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Did not provide asset collection params for asset collection module");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: add more blocks to this check as we support creating more types of modules to this
|
// TODO: add more blocks to this check as we support creating more types of modules to this
|
||||||
// resolver
|
// resolver
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
package com.linkedin.datahub.graphql.types.module;
|
package com.linkedin.datahub.graphql.types.module;
|
||||||
|
|
||||||
|
import com.linkedin.common.urn.Urn;
|
||||||
import com.linkedin.datahub.graphql.QueryContext;
|
import com.linkedin.datahub.graphql.QueryContext;
|
||||||
|
import com.linkedin.datahub.graphql.generated.AssetCollectionModuleParams;
|
||||||
import com.linkedin.datahub.graphql.generated.EntityType;
|
import com.linkedin.datahub.graphql.generated.EntityType;
|
||||||
import com.linkedin.datahub.graphql.generated.LinkModuleParams;
|
import com.linkedin.datahub.graphql.generated.LinkModuleParams;
|
||||||
import com.linkedin.datahub.graphql.generated.Post;
|
import com.linkedin.datahub.graphql.generated.Post;
|
||||||
import com.linkedin.datahub.graphql.generated.RichTextModuleParams;
|
import com.linkedin.datahub.graphql.generated.RichTextModuleParams;
|
||||||
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
|
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
|
||||||
import com.linkedin.module.DataHubPageModuleParams;
|
import com.linkedin.module.DataHubPageModuleParams;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
@ -44,6 +48,21 @@ public class PageModuleParamsMapper
|
|||||||
result.setRichTextParams(richTextParams);
|
result.setRichTextParams(richTextParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map asset collection params if present
|
||||||
|
if (params.getAssetCollectionParams() != null
|
||||||
|
&& params.getAssetCollectionParams().getAssetUrns() != null) {
|
||||||
|
|
||||||
|
AssetCollectionModuleParams assetCollectionParams = new AssetCollectionModuleParams();
|
||||||
|
|
||||||
|
List<String> assetUrnStrings =
|
||||||
|
params.getAssetCollectionParams().getAssetUrns().stream()
|
||||||
|
.map(Urn::toString)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
assetCollectionParams.setAssetUrns(assetUrnStrings);
|
||||||
|
result.setAssetCollectionParams(assetCollectionParams);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,6 +78,11 @@ input PageModuleParamsInput {
|
|||||||
The params required if the module is type RICH_TEXT
|
The params required if the module is type RICH_TEXT
|
||||||
"""
|
"""
|
||||||
richTextParams: RichTextModuleParamsInput
|
richTextParams: RichTextModuleParamsInput
|
||||||
|
|
||||||
|
"""
|
||||||
|
The params required if the module is type ASSET_COLLECTION
|
||||||
|
"""
|
||||||
|
assetCollectionParams: AssetCollectionModuleParamsInput
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -100,6 +105,16 @@ input RichTextModuleParamsInput {
|
|||||||
content: String!
|
content: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for the params required if the module is type ASSET_COLLECTION
|
||||||
|
"""
|
||||||
|
input AssetCollectionModuleParamsInput {
|
||||||
|
"""
|
||||||
|
The list of asset urns for the asset collection module
|
||||||
|
"""
|
||||||
|
assetUrns: [String!]!
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The main properties of a DataHub page module
|
The main properties of a DataHub page module
|
||||||
"""
|
"""
|
||||||
@ -202,6 +217,11 @@ type DataHubPageModuleParams {
|
|||||||
The params required if the module is type RICH_TEXT
|
The params required if the module is type RICH_TEXT
|
||||||
"""
|
"""
|
||||||
richTextParams: RichTextModuleParams
|
richTextParams: RichTextModuleParams
|
||||||
|
|
||||||
|
"""
|
||||||
|
The params required if the module is type ASSET_COLLECTION
|
||||||
|
"""
|
||||||
|
assetCollectionParams: AssetCollectionModuleParams
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -224,6 +244,16 @@ type RichTextModuleParams {
|
|||||||
content: String!
|
content: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The params required if the module is type ASSET_COLLECTION
|
||||||
|
"""
|
||||||
|
type AssetCollectionModuleParams {
|
||||||
|
"""
|
||||||
|
The list of asset urns for the asset collection module
|
||||||
|
"""
|
||||||
|
assetUrns: [String!]!
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Input for deleting a DataHub page module
|
Input for deleting a DataHub page module
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -49,7 +49,9 @@ export const Checkbox = ({
|
|||||||
</Label>
|
</Label>
|
||||||
) : null}
|
) : null}
|
||||||
<CheckboxBase
|
<CheckboxBase
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
if (!isDisabled) {
|
if (!isDisabled) {
|
||||||
setChecked(!checked);
|
setChecked(!checked);
|
||||||
setIsChecked?.(!checked);
|
setIsChecked?.(!checked);
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export function useHydratedEntityMap(urns?: (string | undefined | null)[]) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Fetch entities
|
// Fetch entities
|
||||||
const hydratedEntities = useGetEntities(uniqueEntityUrns);
|
const { entities: hydratedEntities } = useGetEntities(uniqueEntityUrns);
|
||||||
|
|
||||||
// Create entity map
|
// Create entity map
|
||||||
const hydratedEntityMap = useMemo(
|
const hydratedEntityMap = useMemo(
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import React, { ReactNode, createContext, useContext, useMemo } from 'react';
|
import React, { ReactNode, createContext, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useModuleModalState } from '@app/homeV3/context/hooks/useModuleModalState';
|
||||||
import { useModuleOperations } from '@app/homeV3/context/hooks/useModuleOperations';
|
import { useModuleOperations } from '@app/homeV3/context/hooks/useModuleOperations';
|
||||||
import { useTemplateOperations } from '@app/homeV3/context/hooks/useTemplateOperations';
|
import { useTemplateOperations } from '@app/homeV3/context/hooks/useTemplateOperations';
|
||||||
import { useTemplateState } from '@app/homeV3/context/hooks/useTemplateState';
|
import { useTemplateState } from '@app/homeV3/context/hooks/useTemplateState';
|
||||||
@ -33,8 +34,11 @@ export const PageTemplateProvider = ({
|
|||||||
// Template operations
|
// Template operations
|
||||||
const { updateTemplateWithModule, removeModuleFromTemplate, upsertTemplate } = useTemplateOperations();
|
const { updateTemplateWithModule, removeModuleFromTemplate, upsertTemplate } = useTemplateOperations();
|
||||||
|
|
||||||
|
// Modal state
|
||||||
|
const moduleModalState = useModuleModalState();
|
||||||
|
|
||||||
// Module operations
|
// Module operations
|
||||||
const { addModule, removeModule, createModule } = useModuleOperations(
|
const { addModule, removeModule, upsertModule } = useModuleOperations(
|
||||||
isEditingGlobalTemplate,
|
isEditingGlobalTemplate,
|
||||||
personalTemplate,
|
personalTemplate,
|
||||||
globalTemplate,
|
globalTemplate,
|
||||||
@ -43,6 +47,7 @@ export const PageTemplateProvider = ({
|
|||||||
updateTemplateWithModule,
|
updateTemplateWithModule,
|
||||||
removeModuleFromTemplate,
|
removeModuleFromTemplate,
|
||||||
upsertTemplate,
|
upsertTemplate,
|
||||||
|
moduleModalState.isEditing,
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
@ -57,7 +62,8 @@ export const PageTemplateProvider = ({
|
|||||||
setTemplate,
|
setTemplate,
|
||||||
addModule,
|
addModule,
|
||||||
removeModule,
|
removeModule,
|
||||||
createModule,
|
upsertModule,
|
||||||
|
moduleModalState,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
personalTemplate,
|
personalTemplate,
|
||||||
@ -70,7 +76,8 @@ export const PageTemplateProvider = ({
|
|||||||
setTemplate,
|
setTemplate,
|
||||||
addModule,
|
addModule,
|
||||||
removeModule,
|
removeModule,
|
||||||
createModule,
|
upsertModule,
|
||||||
|
moduleModalState,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -86,4 +93,4 @@ export function usePageTemplateContext() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-export types for convenience
|
// Re-export types for convenience
|
||||||
export type { CreateModuleInput, AddModuleInput, RemoveModuleInput } from './types';
|
export type { UpsertModuleInput, AddModuleInput, RemoveModuleInput } from './types';
|
||||||
|
|||||||
@ -87,7 +87,7 @@ const mockSetGlobalTemplate = vi.fn();
|
|||||||
const mockSetTemplate = vi.fn();
|
const mockSetTemplate = vi.fn();
|
||||||
const mockAddModule = vi.fn();
|
const mockAddModule = vi.fn();
|
||||||
const mockRemoveModule = vi.fn();
|
const mockRemoveModule = vi.fn();
|
||||||
const mockCreateModule = vi.fn();
|
const mockUpsertModule = vi.fn();
|
||||||
const mockUpdateTemplateWithModule = vi.fn();
|
const mockUpdateTemplateWithModule = vi.fn();
|
||||||
const mockRemoveModuleFromTemplate = vi.fn();
|
const mockRemoveModuleFromTemplate = vi.fn();
|
||||||
const mockUpsertTemplate = vi.fn();
|
const mockUpsertTemplate = vi.fn();
|
||||||
@ -117,7 +117,7 @@ describe('PageTemplateContext', () => {
|
|||||||
mockUseModuleOperations.mockReturnValue({
|
mockUseModuleOperations.mockReturnValue({
|
||||||
addModule: mockAddModule,
|
addModule: mockAddModule,
|
||||||
removeModule: mockRemoveModule,
|
removeModule: mockRemoveModule,
|
||||||
createModule: mockCreateModule,
|
upsertModule: mockUpsertModule,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -183,6 +183,7 @@ describe('PageTemplateContext', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -281,6 +282,7 @@ describe('PageTemplateContext', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -328,7 +330,13 @@ describe('PageTemplateContext', () => {
|
|||||||
expect(result.current.setGlobalTemplate).toBe(mockSetGlobalTemplate);
|
expect(result.current.setGlobalTemplate).toBe(mockSetGlobalTemplate);
|
||||||
expect(result.current.setTemplate).toBe(mockSetTemplate);
|
expect(result.current.setTemplate).toBe(mockSetTemplate);
|
||||||
expect(result.current.addModule).toBe(mockAddModule);
|
expect(result.current.addModule).toBe(mockAddModule);
|
||||||
expect(result.current.createModule).toBe(mockCreateModule);
|
expect(result.current.upsertModule).toBe(mockUpsertModule);
|
||||||
|
expect(result.current.moduleModalState).toMatchObject({
|
||||||
|
isOpen: false,
|
||||||
|
isEditing: false,
|
||||||
|
open: expect.any(Function),
|
||||||
|
close: expect.any(Function),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error when used outside provider', () => {
|
it('should throw error when used outside provider', () => {
|
||||||
@ -372,7 +380,7 @@ describe('PageTemplateContext', () => {
|
|||||||
expect(mockAddModule).toHaveBeenCalledWith(moduleInput);
|
expect(mockAddModule).toHaveBeenCalledWith(moduleInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should provide working createModule function', () => {
|
it('should provide working upsertModule function', () => {
|
||||||
const { result } = renderHook(() => usePageTemplateContext(), {
|
const { result } = renderHook(() => usePageTemplateContext(), {
|
||||||
wrapper: ({ children }) => (
|
wrapper: ({ children }) => (
|
||||||
<PageTemplateProvider personalTemplate={mockPersonalTemplate} globalTemplate={mockGlobalTemplate}>
|
<PageTemplateProvider personalTemplate={mockPersonalTemplate} globalTemplate={mockGlobalTemplate}>
|
||||||
@ -381,7 +389,7 @@ describe('PageTemplateContext', () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createModuleInput = {
|
const upsertModuleInput = {
|
||||||
name: 'New Module',
|
name: 'New Module',
|
||||||
type: DataHubPageModuleType.Link,
|
type: DataHubPageModuleType.Link,
|
||||||
scope: PageModuleScope.Personal,
|
scope: PageModuleScope.Personal,
|
||||||
@ -393,10 +401,10 @@ describe('PageTemplateContext', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.createModule(createModuleInput);
|
result.current.upsertModule(upsertModuleInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockCreateModule).toHaveBeenCalledWith(createModuleInput);
|
expect(mockUpsertModule).toHaveBeenCalledWith(upsertModuleInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should provide working setIsEditingGlobalTemplate function', () => {
|
it('should provide working setIsEditingGlobalTemplate function', () => {
|
||||||
|
|||||||
@ -0,0 +1,175 @@
|
|||||||
|
import { act, renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { useModuleModalState } from '@app/homeV3/context/hooks/useModuleModalState';
|
||||||
|
import type { ModulePositionInput } from '@app/homeV3/template/types';
|
||||||
|
|
||||||
|
import type { PageModuleFragment } from '@graphql/template.generated';
|
||||||
|
import { DataHubPageModuleType, EntityType, PageModuleScope } from '@types';
|
||||||
|
|
||||||
|
const mockModule: PageModuleFragment = {
|
||||||
|
urn: 'urn:li:pageModule:new',
|
||||||
|
type: EntityType.DatahubPageModule,
|
||||||
|
properties: {
|
||||||
|
name: 'New Module',
|
||||||
|
type: DataHubPageModuleType.AssetCollection,
|
||||||
|
visibility: { scope: PageModuleScope.Personal },
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const anotherMockModule: PageModuleFragment = {
|
||||||
|
urn: 'urn:li:pageModule:another',
|
||||||
|
type: EntityType.DatahubPageModule,
|
||||||
|
properties: {
|
||||||
|
name: 'Second Module',
|
||||||
|
type: DataHubPageModuleType.AssetCollection,
|
||||||
|
visibility: { scope: PageModuleScope.Personal },
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const TYPE = DataHubPageModuleType.AssetCollection;
|
||||||
|
const TYPE2 = DataHubPageModuleType.Domains;
|
||||||
|
|
||||||
|
const POS1: ModulePositionInput = { rowIndex: 0, rowSide: 'left' };
|
||||||
|
const POS2: ModulePositionInput = { rowIndex: 1, rowSide: 'right' };
|
||||||
|
const POS3: ModulePositionInput = { rowIndex: 5, rowSide: 'left' };
|
||||||
|
|
||||||
|
describe('useModuleModalState', () => {
|
||||||
|
it('should initialize to default state', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
expect(result.current.isOpen).toBe(false);
|
||||||
|
expect(result.current.moduleType).toBeNull();
|
||||||
|
expect(result.current.position).toBeNull();
|
||||||
|
expect(result.current.isEditing).toBe(false);
|
||||||
|
expect(result.current.initialState).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set correct state on open()', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.open(TYPE, POS1));
|
||||||
|
expect(result.current.isOpen).toBe(true);
|
||||||
|
expect(result.current.moduleType).toBe(TYPE);
|
||||||
|
expect(result.current.position).toEqual(POS1);
|
||||||
|
expect(result.current.isEditing).toBe(false);
|
||||||
|
expect(result.current.initialState).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update state to latest args on consecutive open()', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.open(TYPE, POS1));
|
||||||
|
act(() => result.current.open(TYPE2, POS2));
|
||||||
|
expect(result.current.moduleType).toBe(TYPE2);
|
||||||
|
expect(result.current.position).toEqual(POS2);
|
||||||
|
expect(result.current.isOpen).toBe(true);
|
||||||
|
expect(result.current.isEditing).toBe(false);
|
||||||
|
expect(result.current.initialState).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different position values on open()', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.open(TYPE2, POS3));
|
||||||
|
expect(result.current.position).toEqual(POS3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set edit state module on openToEdit()', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.openToEdit(TYPE, mockModule));
|
||||||
|
expect(result.current.isOpen).toBe(true);
|
||||||
|
expect(result.current.moduleType).toBe(TYPE);
|
||||||
|
expect(result.current.isEditing).toBe(true);
|
||||||
|
expect(result.current.initialState).toBe(mockModule);
|
||||||
|
expect(result.current.position).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update to latest args on consecutive openToEdit()', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.openToEdit(TYPE, mockModule));
|
||||||
|
act(() => result.current.openToEdit(TYPE2, anotherMockModule));
|
||||||
|
expect(result.current.isOpen).toBe(true);
|
||||||
|
expect(result.current.moduleType).toBe(TYPE2);
|
||||||
|
expect(result.current.isEditing).toBe(true);
|
||||||
|
expect(result.current.initialState).toBe(anotherMockModule);
|
||||||
|
expect(result.current.position).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset state after open() then close()', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.open(TYPE, POS1));
|
||||||
|
act(() => result.current.close());
|
||||||
|
expect(result.current.isOpen).toBe(false);
|
||||||
|
expect(result.current.moduleType).toBeNull();
|
||||||
|
expect(result.current.position).toBeNull();
|
||||||
|
expect(result.current.isEditing).toBe(false);
|
||||||
|
expect(result.current.initialState).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset state after openToEdit() then close()', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.openToEdit(TYPE, mockModule));
|
||||||
|
act(() => result.current.close());
|
||||||
|
expect(result.current.isOpen).toBe(false);
|
||||||
|
expect(result.current.moduleType).toBeNull();
|
||||||
|
expect(result.current.position).toBeNull();
|
||||||
|
expect(result.current.isEditing).toBe(false);
|
||||||
|
expect(result.current.initialState).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset edit state on open() after openToEdit()', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.openToEdit(TYPE, mockModule));
|
||||||
|
act(() => result.current.open(TYPE2, POS2));
|
||||||
|
expect(result.current.isOpen).toBe(true);
|
||||||
|
expect(result.current.moduleType).toBe(TYPE2);
|
||||||
|
expect(result.current.position).toEqual(POS2);
|
||||||
|
expect(result.current.isEditing).toBe(false);
|
||||||
|
expect(result.current.initialState).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should maintain correct state on multiple alternations between open and openToEdit', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.open(TYPE, POS1));
|
||||||
|
expect(result.current.position).toEqual(POS1);
|
||||||
|
act(() => result.current.openToEdit(TYPE, mockModule));
|
||||||
|
expect(result.current.position).toEqual(POS1);
|
||||||
|
act(() => result.current.open(TYPE, POS2));
|
||||||
|
expect(result.current.position).toEqual(POS2);
|
||||||
|
act(() => result.current.openToEdit(TYPE2, mockModule));
|
||||||
|
expect(result.current.position).toEqual(POS2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fully reset all state after a complex sequence and close()', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.open(TYPE, POS1));
|
||||||
|
act(() => result.current.openToEdit(TYPE2, mockModule));
|
||||||
|
act(() => result.current.open(TYPE, POS2));
|
||||||
|
act(() => result.current.close());
|
||||||
|
expect(result.current.isOpen).toBe(false);
|
||||||
|
expect(result.current.moduleType).toBeNull();
|
||||||
|
expect(result.current.position).toBeNull();
|
||||||
|
expect(result.current.isEditing).toBe(false);
|
||||||
|
expect(result.current.initialState).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw and always reset state on multiple close() calls', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.open(TYPE, POS1));
|
||||||
|
act(() => result.current.close());
|
||||||
|
act(() => result.current.close());
|
||||||
|
expect(result.current.isOpen).toBe(false);
|
||||||
|
expect(result.current.moduleType).toBeNull();
|
||||||
|
expect(result.current.position).toBeNull();
|
||||||
|
expect(result.current.isEditing).toBe(false);
|
||||||
|
expect(result.current.initialState).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retain latest initialState and position after successive edits', () => {
|
||||||
|
const { result } = renderHook(() => useModuleModalState());
|
||||||
|
act(() => result.current.open(TYPE, POS1));
|
||||||
|
act(() => result.current.openToEdit(TYPE, mockModule));
|
||||||
|
act(() => result.current.openToEdit(TYPE, anotherMockModule));
|
||||||
|
expect(result.current.initialState).toBe(anotherMockModule);
|
||||||
|
expect(result.current.position).toEqual(POS1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -107,6 +107,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false, // isEditingModule
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -137,7 +138,12 @@ describe('useModuleOperations', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUpdateTemplateWithModule).toHaveBeenCalledWith(mockPersonalTemplate, mockModule, position);
|
expect(mockUpdateTemplateWithModule).toHaveBeenCalledWith(
|
||||||
|
mockPersonalTemplate,
|
||||||
|
mockModule,
|
||||||
|
position,
|
||||||
|
false,
|
||||||
|
);
|
||||||
expect(mockSetPersonalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
expect(mockSetPersonalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
||||||
expect(mockUpsertTemplate).toHaveBeenCalledWith(updatedTemplate, true, mockPersonalTemplate);
|
expect(mockUpsertTemplate).toHaveBeenCalledWith(updatedTemplate, true, mockPersonalTemplate);
|
||||||
});
|
});
|
||||||
@ -153,6 +159,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false, // isEditingModule
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -183,7 +190,7 @@ describe('useModuleOperations', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUpdateTemplateWithModule).toHaveBeenCalledWith(mockGlobalTemplate, mockModule, position);
|
expect(mockUpdateTemplateWithModule).toHaveBeenCalledWith(mockGlobalTemplate, mockModule, position, false);
|
||||||
expect(mockSetGlobalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
expect(mockSetGlobalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
||||||
expect(mockUpsertTemplate).toHaveBeenCalledWith(updatedTemplate, false, mockPersonalTemplate);
|
expect(mockUpsertTemplate).toHaveBeenCalledWith(updatedTemplate, false, mockPersonalTemplate);
|
||||||
});
|
});
|
||||||
@ -199,6 +206,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -230,7 +238,7 @@ describe('useModuleOperations', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUpdateTemplateWithModule).toHaveBeenCalledWith(mockGlobalTemplate, mockModule, position);
|
expect(mockUpdateTemplateWithModule).toHaveBeenCalledWith(mockGlobalTemplate, mockModule, position, false);
|
||||||
expect(mockSetPersonalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
expect(mockSetPersonalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
||||||
expect(mockUpsertTemplate).toHaveBeenCalledWith(updatedTemplate, true, null);
|
expect(mockUpsertTemplate).toHaveBeenCalledWith(updatedTemplate, true, null);
|
||||||
});
|
});
|
||||||
@ -246,6 +254,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -284,7 +293,12 @@ describe('useModuleOperations', () => {
|
|||||||
setTimeout(resolve, 0);
|
setTimeout(resolve, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUpdateTemplateWithModule).toHaveBeenCalledWith(mockPersonalTemplate, mockModule, position);
|
expect(mockUpdateTemplateWithModule).toHaveBeenCalledWith(
|
||||||
|
mockPersonalTemplate,
|
||||||
|
mockModule,
|
||||||
|
position,
|
||||||
|
false,
|
||||||
|
);
|
||||||
expect(mockSetPersonalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
expect(mockSetPersonalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
||||||
expect(mockUpsertTemplate).toHaveBeenCalledWith(updatedTemplate, true, mockPersonalTemplate);
|
expect(mockUpsertTemplate).toHaveBeenCalledWith(updatedTemplate, true, mockPersonalTemplate);
|
||||||
expect(mockSetPersonalTemplate).toHaveBeenCalledWith(mockPersonalTemplate); // Revert call
|
expect(mockSetPersonalTemplate).toHaveBeenCalledWith(mockPersonalTemplate); // Revert call
|
||||||
@ -306,6 +320,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false, // isEditingModule
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -355,6 +370,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false, // isEditingModule
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -404,6 +420,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false, // isEditingModule
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -456,6 +473,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false, // isEditingModule
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -512,6 +530,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false, // isEditingModule
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -556,6 +575,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false, // isEditingModule
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -594,6 +614,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false, // isEditingModule
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -638,6 +659,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false, // isEditingModule
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -666,7 +688,7 @@ describe('useModuleOperations', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createModule', () => {
|
describe('upsertModule', () => {
|
||||||
it('should create module and add it to template', async () => {
|
it('should create module and add it to template', async () => {
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useModuleOperations(
|
useModuleOperations(
|
||||||
@ -678,6 +700,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -686,7 +709,7 @@ describe('useModuleOperations', () => {
|
|||||||
rowSide: 'left',
|
rowSide: 'left',
|
||||||
};
|
};
|
||||||
|
|
||||||
const createModuleInput = {
|
const upsertModuleInput = {
|
||||||
name: 'Test Module',
|
name: 'Test Module',
|
||||||
type: DataHubPageModuleType.Link,
|
type: DataHubPageModuleType.Link,
|
||||||
scope: PageModuleScope.Personal,
|
scope: PageModuleScope.Personal,
|
||||||
@ -731,7 +754,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpsertTemplate.mockResolvedValue({});
|
mockUpsertTemplate.mockResolvedValue({});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
result.current.createModule(createModuleInput);
|
result.current.upsertModule(upsertModuleInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUpsertPageModuleMutation).toHaveBeenCalledWith({
|
expect(mockUpsertPageModuleMutation).toHaveBeenCalledWith({
|
||||||
@ -740,10 +763,8 @@ describe('useModuleOperations', () => {
|
|||||||
name: 'Test Module',
|
name: 'Test Module',
|
||||||
type: DataHubPageModuleType.Link,
|
type: DataHubPageModuleType.Link,
|
||||||
scope: PageModuleScope.Personal,
|
scope: PageModuleScope.Personal,
|
||||||
visibility: {
|
|
||||||
scope: PageModuleScope.Personal,
|
|
||||||
},
|
|
||||||
params: { limit: 10 },
|
params: { limit: 10 },
|
||||||
|
urn: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -761,6 +782,7 @@ describe('useModuleOperations', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
position,
|
position,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockSetPersonalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
expect(mockSetPersonalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
||||||
@ -778,6 +800,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -786,7 +809,7 @@ describe('useModuleOperations', () => {
|
|||||||
rowSide: 'right',
|
rowSide: 'right',
|
||||||
};
|
};
|
||||||
|
|
||||||
const createModuleInput = {
|
const upsertModuleInput = {
|
||||||
name: 'Test Module',
|
name: 'Test Module',
|
||||||
type: DataHubPageModuleType.Link,
|
type: DataHubPageModuleType.Link,
|
||||||
position,
|
position,
|
||||||
@ -805,7 +828,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpsertTemplate.mockResolvedValue({});
|
mockUpsertTemplate.mockResolvedValue({});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
result.current.createModule(createModuleInput);
|
await result.current.upsertModule(upsertModuleInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUpsertPageModuleMutation).toHaveBeenCalledWith({
|
expect(mockUpsertPageModuleMutation).toHaveBeenCalledWith({
|
||||||
@ -814,10 +837,8 @@ describe('useModuleOperations', () => {
|
|||||||
name: 'Test Module',
|
name: 'Test Module',
|
||||||
type: DataHubPageModuleType.Link,
|
type: DataHubPageModuleType.Link,
|
||||||
scope: PageModuleScope.Personal, // Default scope
|
scope: PageModuleScope.Personal, // Default scope
|
||||||
visibility: {
|
|
||||||
scope: PageModuleScope.Personal,
|
|
||||||
},
|
|
||||||
params: {}, // Default empty params
|
params: {}, // Default empty params
|
||||||
|
urn: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -834,6 +855,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -842,7 +864,7 @@ describe('useModuleOperations', () => {
|
|||||||
rowSide: 'left',
|
rowSide: 'left',
|
||||||
};
|
};
|
||||||
|
|
||||||
const createModuleInput = {
|
const upsertModuleInput = {
|
||||||
name: 'Test Module',
|
name: 'Test Module',
|
||||||
type: DataHubPageModuleType.Link,
|
type: DataHubPageModuleType.Link,
|
||||||
position,
|
position,
|
||||||
@ -863,7 +885,7 @@ describe('useModuleOperations', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
result.current.createModule(createModuleInput);
|
result.current.upsertModule(upsertModuleInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for the async operation to complete
|
// Wait for the async operation to complete
|
||||||
@ -888,6 +910,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -896,7 +919,7 @@ describe('useModuleOperations', () => {
|
|||||||
rowSide: 'left',
|
rowSide: 'left',
|
||||||
};
|
};
|
||||||
|
|
||||||
const createModuleInput = {
|
const upsertModuleInput = {
|
||||||
name: 'Global Module',
|
name: 'Global Module',
|
||||||
type: DataHubPageModuleType.Link,
|
type: DataHubPageModuleType.Link,
|
||||||
scope: PageModuleScope.Global,
|
scope: PageModuleScope.Global,
|
||||||
@ -940,7 +963,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpsertTemplate.mockResolvedValue({});
|
mockUpsertTemplate.mockResolvedValue({});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
result.current.createModule(createModuleInput);
|
result.current.upsertModule(upsertModuleInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockUpsertPageModuleMutation).toHaveBeenCalledWith({
|
expect(mockUpsertPageModuleMutation).toHaveBeenCalledWith({
|
||||||
@ -949,10 +972,8 @@ describe('useModuleOperations', () => {
|
|||||||
name: 'Global Module',
|
name: 'Global Module',
|
||||||
type: DataHubPageModuleType.Link,
|
type: DataHubPageModuleType.Link,
|
||||||
scope: PageModuleScope.Global,
|
scope: PageModuleScope.Global,
|
||||||
visibility: {
|
|
||||||
scope: PageModuleScope.Global,
|
|
||||||
},
|
|
||||||
params: {},
|
params: {},
|
||||||
|
urn: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -970,6 +991,7 @@ describe('useModuleOperations', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
position,
|
position,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockSetGlobalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
expect(mockSetGlobalTemplate).toHaveBeenCalledWith(updatedTemplate);
|
||||||
@ -990,6 +1012,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false,
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
initialProps: {
|
initialProps: {
|
||||||
@ -1011,7 +1034,7 @@ describe('useModuleOperations', () => {
|
|||||||
expect(result.current.addModule).not.toBe(initialAddModule);
|
expect(result.current.addModule).not.toBe(initialAddModule);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update createModule when dependencies change', () => {
|
it('should update upsertModule when dependencies change', () => {
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(
|
||||||
({ isEditingGlobalTemplate, personalTemplate, globalTemplate }) =>
|
({ isEditingGlobalTemplate, personalTemplate, globalTemplate }) =>
|
||||||
useModuleOperations(
|
useModuleOperations(
|
||||||
@ -1023,6 +1046,7 @@ describe('useModuleOperations', () => {
|
|||||||
mockUpdateTemplateWithModule,
|
mockUpdateTemplateWithModule,
|
||||||
mockRemoveModuleFromTemplate,
|
mockRemoveModuleFromTemplate,
|
||||||
mockUpsertTemplate,
|
mockUpsertTemplate,
|
||||||
|
false,
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
initialProps: {
|
initialProps: {
|
||||||
@ -1033,7 +1057,7 @@ describe('useModuleOperations', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialCreateModule = result.current.createModule;
|
const initialUpsertModule = result.current.upsertModule;
|
||||||
|
|
||||||
rerender({
|
rerender({
|
||||||
isEditingGlobalTemplate: true,
|
isEditingGlobalTemplate: true,
|
||||||
@ -1041,7 +1065,7 @@ describe('useModuleOperations', () => {
|
|||||||
globalTemplate: mockGlobalTemplate,
|
globalTemplate: mockGlobalTemplate,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.createModule).not.toBe(initialCreateModule);
|
expect(result.current.upsertModule).not.toBe(initialUpsertModule);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -68,7 +68,7 @@ describe('useTemplateOperations', () => {
|
|||||||
rowSide: 'left',
|
rowSide: 'left',
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedTemplate = result.current.updateTemplateWithModule(mockTemplate, mockModule, position);
|
const updatedTemplate = result.current.updateTemplateWithModule(mockTemplate, mockModule, position, false);
|
||||||
|
|
||||||
expect(updatedTemplate).not.toBeNull();
|
expect(updatedTemplate).not.toBeNull();
|
||||||
expect(updatedTemplate?.properties?.rows).toHaveLength(2);
|
expect(updatedTemplate?.properties?.rows).toHaveLength(2);
|
||||||
@ -84,7 +84,7 @@ describe('useTemplateOperations', () => {
|
|||||||
rowSide: 'left',
|
rowSide: 'left',
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedTemplate = result.current.updateTemplateWithModule(mockTemplate, mockModule, position);
|
const updatedTemplate = result.current.updateTemplateWithModule(mockTemplate, mockModule, position, false);
|
||||||
|
|
||||||
expect(updatedTemplate).not.toBeNull();
|
expect(updatedTemplate).not.toBeNull();
|
||||||
expect(updatedTemplate?.properties?.rows).toHaveLength(1);
|
expect(updatedTemplate?.properties?.rows).toHaveLength(1);
|
||||||
@ -101,7 +101,7 @@ describe('useTemplateOperations', () => {
|
|||||||
rowSide: 'right',
|
rowSide: 'right',
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedTemplate = result.current.updateTemplateWithModule(mockTemplate, mockModule, position);
|
const updatedTemplate = result.current.updateTemplateWithModule(mockTemplate, mockModule, position, false);
|
||||||
|
|
||||||
expect(updatedTemplate).not.toBeNull();
|
expect(updatedTemplate).not.toBeNull();
|
||||||
expect(updatedTemplate?.properties?.rows).toHaveLength(1);
|
expect(updatedTemplate?.properties?.rows).toHaveLength(1);
|
||||||
@ -118,7 +118,7 @@ describe('useTemplateOperations', () => {
|
|||||||
rowSide: 'left',
|
rowSide: 'left',
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedTemplate = result.current.updateTemplateWithModule(mockTemplate, mockModule, position);
|
const updatedTemplate = result.current.updateTemplateWithModule(mockTemplate, mockModule, position, false);
|
||||||
|
|
||||||
expect(updatedTemplate).not.toBeNull();
|
expect(updatedTemplate).not.toBeNull();
|
||||||
expect(updatedTemplate?.properties?.rows).toHaveLength(2);
|
expect(updatedTemplate?.properties?.rows).toHaveLength(2);
|
||||||
@ -148,7 +148,12 @@ describe('useTemplateOperations', () => {
|
|||||||
rowSide: 'left',
|
rowSide: 'left',
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedTemplate = result.current.updateTemplateWithModule(templateWithoutRows, mockModule, position);
|
const updatedTemplate = result.current.updateTemplateWithModule(
|
||||||
|
templateWithoutRows,
|
||||||
|
mockModule,
|
||||||
|
position,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
expect(updatedTemplate).not.toBeNull();
|
expect(updatedTemplate).not.toBeNull();
|
||||||
expect(updatedTemplate?.properties?.rows).toHaveLength(1);
|
expect(updatedTemplate?.properties?.rows).toHaveLength(1);
|
||||||
@ -164,7 +169,7 @@ describe('useTemplateOperations', () => {
|
|||||||
rowSide: 'left',
|
rowSide: 'left',
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedTemplate = result.current.updateTemplateWithModule(null, mockModule, position);
|
const updatedTemplate = result.current.updateTemplateWithModule(null, mockModule, position, false);
|
||||||
|
|
||||||
expect(updatedTemplate).toBeNull();
|
expect(updatedTemplate).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { ModuleModalState } from '@app/homeV3/context/types';
|
||||||
|
import { ModulePositionInput } from '@app/homeV3/template/types';
|
||||||
|
|
||||||
|
import { PageModuleFragment } from '@graphql/template.generated';
|
||||||
|
import { DataHubPageModuleType } from '@types';
|
||||||
|
|
||||||
|
export function useModuleModalState(): ModuleModalState {
|
||||||
|
const [moduleType, setModuleType] = useState<DataHubPageModuleType | null>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
const [position, setPosition] = useState<ModulePositionInput | null>(null);
|
||||||
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
|
const [initialState, setInitialState] = useState<PageModuleFragment | null>(null);
|
||||||
|
|
||||||
|
const open = useCallback((moduleTypeToCreate: DataHubPageModuleType, positionToCreate: ModulePositionInput) => {
|
||||||
|
setModuleType(moduleTypeToCreate);
|
||||||
|
setIsOpen(true);
|
||||||
|
setPosition(positionToCreate);
|
||||||
|
setIsEditing(false);
|
||||||
|
setInitialState(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openToEdit = useCallback((moduleTypeToEdit: DataHubPageModuleType, currentData: PageModuleFragment) => {
|
||||||
|
setModuleType(moduleTypeToEdit);
|
||||||
|
setIsEditing(true);
|
||||||
|
setInitialState(currentData);
|
||||||
|
setIsOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const close = useCallback(() => {
|
||||||
|
setModuleType(null);
|
||||||
|
setPosition(null);
|
||||||
|
setIsOpen(false);
|
||||||
|
setIsEditing(false);
|
||||||
|
setInitialState(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
moduleType,
|
||||||
|
isOpen,
|
||||||
|
position,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
openToEdit,
|
||||||
|
isEditing,
|
||||||
|
initialState,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,19 +1,11 @@
|
|||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { UpsertModuleInput } from '@app/homeV3/context/types';
|
||||||
import { ModulePositionInput } from '@app/homeV3/template/types';
|
import { ModulePositionInput } from '@app/homeV3/template/types';
|
||||||
|
|
||||||
import { PageModuleFragment, PageTemplateFragment, useUpsertPageModuleMutation } from '@graphql/template.generated';
|
import { PageModuleFragment, PageTemplateFragment, useUpsertPageModuleMutation } from '@graphql/template.generated';
|
||||||
import { DataHubPageModuleType, EntityType, PageModuleScope } from '@types';
|
import { EntityType, PageModuleScope } from '@types';
|
||||||
|
|
||||||
// Input types for the methods
|
|
||||||
export interface CreateModuleInput {
|
|
||||||
name: string;
|
|
||||||
type: DataHubPageModuleType;
|
|
||||||
scope?: PageModuleScope;
|
|
||||||
params?: any; // Module-specific parameters
|
|
||||||
position: ModulePositionInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddModuleInput {
|
export interface AddModuleInput {
|
||||||
module: PageModuleFragment;
|
module: PageModuleFragment;
|
||||||
@ -111,7 +103,7 @@ const validateRemoveModuleInput = (input: RemoveModuleInput): string | null => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateCreateModuleInput = (input: CreateModuleInput): string | null => {
|
const validateUpsertModuleInput = (input: UpsertModuleInput): string | null => {
|
||||||
if (!input.name?.trim()) {
|
if (!input.name?.trim()) {
|
||||||
return 'Module name is required';
|
return 'Module name is required';
|
||||||
}
|
}
|
||||||
@ -134,6 +126,7 @@ export function useModuleOperations(
|
|||||||
templateToUpdate: PageTemplateFragment | null,
|
templateToUpdate: PageTemplateFragment | null,
|
||||||
module: PageModuleFragment,
|
module: PageModuleFragment,
|
||||||
position: ModulePositionInput,
|
position: ModulePositionInput,
|
||||||
|
isEditing: boolean,
|
||||||
) => PageTemplateFragment | null,
|
) => PageTemplateFragment | null,
|
||||||
removeModuleFromTemplate: (
|
removeModuleFromTemplate: (
|
||||||
templateToUpdate: PageTemplateFragment | null,
|
templateToUpdate: PageTemplateFragment | null,
|
||||||
@ -145,6 +138,7 @@ export function useModuleOperations(
|
|||||||
isPersonal: boolean,
|
isPersonal: boolean,
|
||||||
personalTemplate: PageTemplateFragment | null,
|
personalTemplate: PageTemplateFragment | null,
|
||||||
) => Promise<any>,
|
) => Promise<any>,
|
||||||
|
isEditingModule: boolean,
|
||||||
) {
|
) {
|
||||||
const [upsertPageModuleMutation] = useUpsertPageModuleMutation();
|
const [upsertPageModuleMutation] = useUpsertPageModuleMutation();
|
||||||
|
|
||||||
@ -189,7 +183,7 @@ export function useModuleOperations(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update template state
|
// Update template state
|
||||||
const updatedTemplate = updateTemplateWithModule(templateToUpdate, module, position);
|
const updatedTemplate = updateTemplateWithModule(templateToUpdate, module, position, isEditingModule);
|
||||||
|
|
||||||
// Update local state immediately for optimistic UI
|
// Update local state immediately for optimistic UI
|
||||||
updateTemplateStateOptimistically(context, updatedTemplate, isPersonal);
|
updateTemplateStateOptimistically(context, updatedTemplate, isPersonal);
|
||||||
@ -197,7 +191,7 @@ export function useModuleOperations(
|
|||||||
// Persist changes
|
// Persist changes
|
||||||
persistTemplateChanges(context, updatedTemplate, isPersonal, 'add module');
|
persistTemplateChanges(context, updatedTemplate, isPersonal, 'add module');
|
||||||
},
|
},
|
||||||
[context, updateTemplateWithModule],
|
[context, isEditingModule, updateTemplateWithModule],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Removes a module from the template state and updates the appropriate template on the backend
|
// Removes a module from the template state and updates the appropriate template on the backend
|
||||||
@ -233,27 +227,25 @@ export function useModuleOperations(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Takes input and makes a call to create a module then add that module to the template
|
// Takes input and makes a call to create a module then add that module to the template
|
||||||
const createModule = useCallback(
|
const upsertModule = useCallback(
|
||||||
(input: CreateModuleInput) => {
|
(input: UpsertModuleInput) => {
|
||||||
// Validate input
|
// Validate input
|
||||||
const validationError = validateCreateModuleInput(input);
|
const validationError = validateUpsertModuleInput(input);
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
console.error('Invalid createModule input:', validationError);
|
console.error('Invalid upsertModule input:', validationError);
|
||||||
message.error(validationError);
|
message.error(validationError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, type, scope = PageModuleScope.Personal, params = {}, position } = input;
|
const { name, type, scope = PageModuleScope.Personal, params = {}, position, urn } = input;
|
||||||
|
|
||||||
// Create the module first
|
// Create the module first
|
||||||
const moduleInput = {
|
const moduleInput = {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
type,
|
type,
|
||||||
scope,
|
scope,
|
||||||
visibility: {
|
|
||||||
scope,
|
|
||||||
},
|
|
||||||
params,
|
params,
|
||||||
|
urn,
|
||||||
};
|
};
|
||||||
|
|
||||||
upsertPageModuleMutation({
|
upsertPageModuleMutation({
|
||||||
@ -262,8 +254,8 @@ export function useModuleOperations(
|
|||||||
.then((moduleResult) => {
|
.then((moduleResult) => {
|
||||||
const moduleUrn = moduleResult.data?.upsertPageModule?.urn;
|
const moduleUrn = moduleResult.data?.upsertPageModule?.urn;
|
||||||
if (!moduleUrn) {
|
if (!moduleUrn) {
|
||||||
console.error('Failed to create module - no URN returned');
|
console.error(`Failed to ${isEditingModule ? 'update' : 'create'} module - no URN returned`);
|
||||||
message.error('Failed to create module');
|
message.error(`Failed to ${isEditingModule ? 'update' : 'create'} module`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,16 +278,16 @@ export function useModuleOperations(
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to create module:', error);
|
console.error(`Failed to ${isEditingModule ? 'update' : 'create'} module:`, error);
|
||||||
message.error('Failed to create module');
|
message.error(`Failed to ${isEditingModule ? 'update' : 'create'} module`);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[upsertPageModuleMutation, addModule],
|
[upsertPageModuleMutation, addModule, isEditingModule],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addModule,
|
addModule,
|
||||||
removeModule,
|
removeModule,
|
||||||
createModule,
|
upsertModule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,11 +52,25 @@ export function useTemplateOperations() {
|
|||||||
templateToUpdate: PageTemplateFragment | null,
|
templateToUpdate: PageTemplateFragment | null,
|
||||||
module: PageModuleFragment,
|
module: PageModuleFragment,
|
||||||
position: ModulePositionInput,
|
position: ModulePositionInput,
|
||||||
|
isEditingModule: boolean,
|
||||||
): PageTemplateFragment | null => {
|
): PageTemplateFragment | null => {
|
||||||
if (!templateToUpdate) return null;
|
if (!templateToUpdate) return null;
|
||||||
|
|
||||||
const newTemplate = { ...templateToUpdate };
|
const newTemplate = { ...templateToUpdate };
|
||||||
const newRows = [...(newTemplate.properties?.rows || [])];
|
let newRows = [...(newTemplate.properties?.rows || [])];
|
||||||
|
|
||||||
|
// Update the existing module in-place for Optimistic UI changes
|
||||||
|
if (isEditingModule && module.urn) {
|
||||||
|
newRows = newRows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
modules: (row.modules || []).map((mod) => (mod.urn === module.urn ? { ...mod, ...module } : mod)),
|
||||||
|
}));
|
||||||
|
newTemplate.properties = {
|
||||||
|
...newTemplate.properties,
|
||||||
|
rows: newRows,
|
||||||
|
};
|
||||||
|
return newTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
if (position.rowIndex === undefined) {
|
if (position.rowIndex === undefined) {
|
||||||
// Add to new row at the end
|
// Add to new row at the end
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import { PageModuleFragment, PageTemplateFragment } from '@graphql/template.gene
|
|||||||
import { DataHubPageModuleType, PageModuleScope } from '@types';
|
import { DataHubPageModuleType, PageModuleScope } from '@types';
|
||||||
|
|
||||||
// Input types for the methods
|
// Input types for the methods
|
||||||
export interface CreateModuleInput {
|
export interface UpsertModuleInput {
|
||||||
|
urn?: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: DataHubPageModuleType;
|
type: DataHubPageModuleType;
|
||||||
scope?: PageModuleScope;
|
scope?: PageModuleScope;
|
||||||
@ -21,6 +22,16 @@ export interface RemoveModuleInput {
|
|||||||
moduleUrn: string;
|
moduleUrn: string;
|
||||||
position: ModulePositionInput;
|
position: ModulePositionInput;
|
||||||
}
|
}
|
||||||
|
export interface ModuleModalState {
|
||||||
|
isOpen: boolean;
|
||||||
|
moduleType: DataHubPageModuleType | null;
|
||||||
|
position: ModulePositionInput | null;
|
||||||
|
open: (moduleType: DataHubPageModuleType, position: ModulePositionInput) => void;
|
||||||
|
close: () => void;
|
||||||
|
isEditing: boolean;
|
||||||
|
initialState: PageModuleFragment | null;
|
||||||
|
openToEdit: (moduleType: DataHubPageModuleType, currentData: PageModuleFragment) => void;
|
||||||
|
}
|
||||||
|
|
||||||
// Context state shape
|
// Context state shape
|
||||||
export type PageTemplateContextState = {
|
export type PageTemplateContextState = {
|
||||||
@ -33,6 +44,7 @@ export type PageTemplateContextState = {
|
|||||||
setGlobalTemplate: (template: PageTemplateFragment | null) => void;
|
setGlobalTemplate: (template: PageTemplateFragment | null) => void;
|
||||||
setTemplate: (template: PageTemplateFragment | null) => void;
|
setTemplate: (template: PageTemplateFragment | null) => void;
|
||||||
addModule: (input: AddModuleInput) => void;
|
addModule: (input: AddModuleInput) => void;
|
||||||
createModule: (input: CreateModuleInput) => void;
|
upsertModule: (input: UpsertModuleInput) => void;
|
||||||
|
moduleModalState: ModuleModalState;
|
||||||
removeModule: (input: RemoveModuleInput) => void;
|
removeModule: (input: RemoveModuleInput) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import React, { useMemo } from 'react';
|
|||||||
import { ModuleProps } from '@app/homeV3/module/types';
|
import { ModuleProps } from '@app/homeV3/module/types';
|
||||||
import SampleLargeModule from '@app/homeV3/modules/SampleLargeModule';
|
import SampleLargeModule from '@app/homeV3/modules/SampleLargeModule';
|
||||||
import YourAssetsModule from '@app/homeV3/modules/YourAssetsModule';
|
import YourAssetsModule from '@app/homeV3/modules/YourAssetsModule';
|
||||||
|
import AssetCollectionModule from '@app/homeV3/modules/assetCollection/AssetCollectionModule';
|
||||||
import TopDomainsModule from '@app/homeV3/modules/domains/TopDomainsModule';
|
import TopDomainsModule from '@app/homeV3/modules/domains/TopDomainsModule';
|
||||||
|
|
||||||
import { DataHubPageModuleType } from '@types';
|
import { DataHubPageModuleType } from '@types';
|
||||||
@ -12,6 +13,7 @@ export default function Module(props: ModuleProps) {
|
|||||||
const Component = useMemo(() => {
|
const Component = useMemo(() => {
|
||||||
if (module.properties.type === DataHubPageModuleType.OwnedAssets) return YourAssetsModule;
|
if (module.properties.type === DataHubPageModuleType.OwnedAssets) return YourAssetsModule;
|
||||||
if (module.properties.type === DataHubPageModuleType.Domains) return TopDomainsModule;
|
if (module.properties.type === DataHubPageModuleType.Domains) return TopDomainsModule;
|
||||||
|
if (module.properties.type === DataHubPageModuleType.AssetCollection) return AssetCollectionModule;
|
||||||
|
|
||||||
// TODO: remove the sample large module once we have other modules to fill this out
|
// TODO: remove the sample large module once we have other modules to fill this out
|
||||||
console.error(`Issue finding module with type ${module.properties.type}`);
|
console.error(`Issue finding module with type ${module.properties.type}`);
|
||||||
|
|||||||
@ -9,14 +9,30 @@ import { Entity } from '@types';
|
|||||||
interface Props {
|
interface Props {
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
customDetailsRenderer?: (entity: Entity) => void;
|
customDetailsRenderer?: (entity: Entity) => void;
|
||||||
|
navigateOnlyOnNameClick?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EntityItem({ entity, customDetailsRenderer }: Props) {
|
export default function EntityItem({ entity, customDetailsRenderer, navigateOnlyOnNameClick = false }: Props) {
|
||||||
const entityRegistry = useEntityRegistryV2();
|
const entityRegistry = useEntityRegistryV2();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={entityRegistry.getEntityUrl(entity.type, entity.urn)}>
|
<>
|
||||||
<AutoCompleteEntityItem entity={entity} key={entity.urn} customDetailsRenderer={customDetailsRenderer} />
|
{navigateOnlyOnNameClick ? (
|
||||||
</Link>
|
<AutoCompleteEntityItem
|
||||||
|
entity={entity}
|
||||||
|
key={entity.urn}
|
||||||
|
customDetailsRenderer={customDetailsRenderer}
|
||||||
|
navigateOnlyOnNameClick
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Link to={entityRegistry.getEntityUrl(entity.type, entity.urn)}>
|
||||||
|
<AutoCompleteEntityItem
|
||||||
|
entity={entity}
|
||||||
|
key={entity.urn}
|
||||||
|
customDetailsRenderer={customDetailsRenderer}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
|
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
|
||||||
|
import { DEFAULT_GLOBAL_MODULE_TYPES } from '@app/homeV3/modules/constants';
|
||||||
import { ModulePositionInput } from '@app/homeV3/template/types';
|
import { ModulePositionInput } from '@app/homeV3/template/types';
|
||||||
|
|
||||||
import { PageModuleFragment } from '@graphql/template.generated';
|
import { PageModuleFragment } from '@graphql/template.generated';
|
||||||
@ -20,7 +21,17 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ModuleMenu({ module, position }: Props) {
|
export default function ModuleMenu({ module, position }: Props) {
|
||||||
const { removeModule } = usePageTemplateContext();
|
const { type } = module.properties;
|
||||||
|
const canEdit = !DEFAULT_GLOBAL_MODULE_TYPES.includes(type);
|
||||||
|
|
||||||
|
const {
|
||||||
|
removeModule,
|
||||||
|
moduleModalState: { openToEdit },
|
||||||
|
} = usePageTemplateContext();
|
||||||
|
|
||||||
|
const handleEditModule = useCallback(() => {
|
||||||
|
openToEdit(type, module);
|
||||||
|
}, [module, openToEdit, type]);
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
removeModule({
|
removeModule({
|
||||||
@ -34,28 +45,27 @@ export default function ModuleMenu({ module, position }: Props) {
|
|||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
menu={{
|
menu={{
|
||||||
items: [
|
items: [
|
||||||
{
|
...(canEdit
|
||||||
title: 'Edit',
|
? [
|
||||||
key: 'edit',
|
{
|
||||||
label: 'Edit',
|
title: 'Edit',
|
||||||
onClick: () => {
|
key: 'edit',
|
||||||
// TODO: Implement edit functionality
|
label: 'Edit',
|
||||||
},
|
style: {
|
||||||
},
|
color: colors.gray[600],
|
||||||
{
|
fontSize: '14px',
|
||||||
title: 'Duplicate',
|
},
|
||||||
label: 'Duplicate',
|
onClick: handleEditModule,
|
||||||
key: 'duplicate',
|
},
|
||||||
onClick: () => {
|
]
|
||||||
// TODO: Implement duplicate functionality
|
: []),
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Delete',
|
title: 'Delete',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
style: {
|
style: {
|
||||||
color: colors.red[500],
|
color: colors.red[500],
|
||||||
|
fontSize: '14px',
|
||||||
},
|
},
|
||||||
onClick: handleDelete,
|
onClick: handleDelete,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,9 +10,11 @@ import { DataHubPageModuleType, EntityType, PageModuleScope } from '@types';
|
|||||||
|
|
||||||
// Mock the PageTemplateContext
|
// Mock the PageTemplateContext
|
||||||
const mockRemoveModule = vi.fn();
|
const mockRemoveModule = vi.fn();
|
||||||
|
const mockOpenToEdit = vi.fn();
|
||||||
vi.mock('@app/homeV3/context/PageTemplateContext', () => ({
|
vi.mock('@app/homeV3/context/PageTemplateContext', () => ({
|
||||||
usePageTemplateContext: () => ({
|
usePageTemplateContext: () => ({
|
||||||
removeModule: mockRemoveModule,
|
removeModule: mockRemoveModule,
|
||||||
|
moduleModalState: { openToEdit: mockOpenToEdit },
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -24,6 +26,9 @@ vi.mock('@components', () => ({
|
|||||||
</div>
|
</div>
|
||||||
)),
|
)),
|
||||||
colors: {
|
colors: {
|
||||||
|
gray: {
|
||||||
|
600: '#4B5563',
|
||||||
|
},
|
||||||
red: {
|
red: {
|
||||||
500: '#ef4444',
|
500: '#ef4444',
|
||||||
},
|
},
|
||||||
@ -60,7 +65,7 @@ describe('ModuleMenu', () => {
|
|||||||
|
|
||||||
// Check that menu items are rendered
|
// Check that menu items are rendered
|
||||||
expect(screen.getByText('Edit')).toBeInTheDocument();
|
expect(screen.getByText('Edit')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Duplicate')).toBeInTheDocument();
|
// expect(screen.getByText('Duplicate')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Delete')).toBeInTheDocument();
|
expect(screen.getByText('Delete')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -234,7 +239,7 @@ describe('ModuleMenu', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle edit and duplicate options (placeholder functionality)', () => {
|
it('should handle edit option (placeholder functionality)', () => {
|
||||||
render(<ModuleMenu module={mockModule} position={mockPosition} />);
|
render(<ModuleMenu module={mockModule} position={mockPosition} />);
|
||||||
|
|
||||||
// Click to open the dropdown
|
// Click to open the dropdown
|
||||||
@ -243,14 +248,14 @@ describe('ModuleMenu', () => {
|
|||||||
|
|
||||||
// Check that edit and duplicate options are present
|
// Check that edit and duplicate options are present
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
const duplicateButton = screen.getByText('Duplicate');
|
// const duplicateButton = screen.getByText('Duplicate');
|
||||||
|
|
||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
expect(duplicateButton).toBeInTheDocument();
|
// expect(duplicateButton).toBeInTheDocument();
|
||||||
|
|
||||||
// Click edit and duplicate (should not throw errors)
|
// Click edit and duplicate (should not throw errors)
|
||||||
fireEvent.click(editButton);
|
fireEvent.click(editButton);
|
||||||
fireEvent.click(duplicateButton);
|
// fireEvent.click(duplicateButton);
|
||||||
|
|
||||||
// These are placeholder implementations, so we just verify they don't crash
|
// These are placeholder implementations, so we just verify they don't crash
|
||||||
expect(mockRemoveModule).not.toHaveBeenCalled();
|
expect(mockRemoveModule).not.toHaveBeenCalled();
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
|
||||||
|
import AssetCollectionModal from '@app/homeV3/modules/assetCollection/AssetCollectionModal';
|
||||||
|
|
||||||
|
import { DataHubPageModuleType } from '@types';
|
||||||
|
|
||||||
|
export default function ModuleModalMapper() {
|
||||||
|
const {
|
||||||
|
moduleModalState: { moduleType },
|
||||||
|
} = usePageTemplateContext();
|
||||||
|
|
||||||
|
const ModuleModalComponent = useMemo(() => {
|
||||||
|
switch (moduleType) {
|
||||||
|
// TODO: add support of other module types
|
||||||
|
case DataHubPageModuleType.AssetCollection:
|
||||||
|
return AssetCollectionModal;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [moduleType]);
|
||||||
|
|
||||||
|
if (moduleType === undefined) return null;
|
||||||
|
if (!ModuleModalComponent) return null;
|
||||||
|
|
||||||
|
return <ModuleModalComponent />;
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { Modal } from '@components';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ModalButton } from '@components/components/Modal/Modal';
|
||||||
|
|
||||||
|
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
|
||||||
|
|
||||||
|
const modalStyles = {
|
||||||
|
maxWidth: '1000px',
|
||||||
|
minWidth: '800px',
|
||||||
|
maxHeight: '90%',
|
||||||
|
};
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
onUpsert: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BaseModuleModal({ title, subtitle, children, onUpsert }: React.PropsWithChildren<Props>) {
|
||||||
|
const {
|
||||||
|
moduleModalState: { close, isOpen, isEditing },
|
||||||
|
} = usePageTemplateContext();
|
||||||
|
|
||||||
|
// Modal buttons configuration
|
||||||
|
const buttons: ModalButton[] = [
|
||||||
|
{
|
||||||
|
text: 'Cancel',
|
||||||
|
color: 'primary',
|
||||||
|
variant: 'text',
|
||||||
|
onClick: close,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `${isEditing ? 'Update' : 'Create'}`,
|
||||||
|
color: 'primary',
|
||||||
|
variant: 'filled',
|
||||||
|
onClick: onUpsert,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={isOpen}
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
buttons={buttons}
|
||||||
|
onCancel={close}
|
||||||
|
maskClosable={false} // to avoid accidental clicks that closes the modal
|
||||||
|
style={modalStyles}
|
||||||
|
width="90%"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { Input } from '@components';
|
||||||
|
import { Form, FormInstance } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FormValues } from '@app/homeV3/modules/assetCollection/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
form: FormInstance;
|
||||||
|
formValues?: FormValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModuleDetailsForm = ({ form, formValues }: Props) => {
|
||||||
|
return (
|
||||||
|
<Form form={form} initialValues={formValues}>
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please enter the name',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input label="Name" placeholder="Choose a name for your widget" isRequired />
|
||||||
|
</Form.Item>
|
||||||
|
{/* Should be used later, once support for description is added */}
|
||||||
|
{/* <Form.Item name="description">
|
||||||
|
<TextArea label="Description" placeholder="Help others understand what this collection contains..." />
|
||||||
|
</Form.Item> */}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModuleDetailsForm;
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import { Form } from 'antd';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
|
||||||
|
import BaseModuleModal from '@app/homeV3/moduleModals/common/BaseModuleModal';
|
||||||
|
import ModuleDetailsForm from '@app/homeV3/moduleModals/common/ModuleDetailsForm';
|
||||||
|
import AssetsSection from '@app/homeV3/modules/assetCollection/AssetsSection';
|
||||||
|
|
||||||
|
import { DataHubPageModuleType } from '@types';
|
||||||
|
|
||||||
|
const ModalContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AssetCollectionModal = () => {
|
||||||
|
const {
|
||||||
|
upsertModule,
|
||||||
|
moduleModalState: { position, close, isEditing, initialState },
|
||||||
|
} = usePageTemplateContext();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const currentName = initialState?.properties.name || '';
|
||||||
|
const currentAssets = (initialState?.properties?.params?.assetCollectionParams?.assetUrns || []).filter(
|
||||||
|
(urn): urn is string => typeof urn === 'string',
|
||||||
|
);
|
||||||
|
const urn = initialState?.urn;
|
||||||
|
const [selectedAssetUrns, setSelectedAssetUrns] = useState<string[]>(currentAssets);
|
||||||
|
|
||||||
|
const handleUpsertAssetCollectionModule = () => {
|
||||||
|
form.validateFields().then((values) => {
|
||||||
|
const { name } = values;
|
||||||
|
upsertModule({
|
||||||
|
urn,
|
||||||
|
name,
|
||||||
|
position: position ?? {},
|
||||||
|
type: DataHubPageModuleType.AssetCollection,
|
||||||
|
params: {
|
||||||
|
assetCollectionParams: {
|
||||||
|
assetUrns: selectedAssetUrns,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseModuleModal
|
||||||
|
title={`${isEditing ? 'Edit' : 'Add'} Asset Collection`}
|
||||||
|
subtitle="Create a widget by selecting assets and information that will be shown to your users"
|
||||||
|
onUpsert={handleUpsertAssetCollectionModule}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModuleDetailsForm form={form} formValues={{ name: currentName }} />
|
||||||
|
<AssetsSection selectedAssetUrns={selectedAssetUrns} setSelectedAssetUrns={setSelectedAssetUrns} />
|
||||||
|
</ModalContent>
|
||||||
|
</BaseModuleModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssetCollectionModal;
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import EmptyContent from '@app/homeV3/module/components/EmptyContent';
|
||||||
|
import EntityItem from '@app/homeV3/module/components/EntityItem';
|
||||||
|
import LargeModule from '@app/homeV3/module/components/LargeModule';
|
||||||
|
import { ModuleProps } from '@app/homeV3/module/types';
|
||||||
|
import { useGetEntities } from '@app/sharedV2/useGetEntities';
|
||||||
|
|
||||||
|
import { Entity } from '@types';
|
||||||
|
|
||||||
|
const AssetCollectionModule = (props: ModuleProps) => {
|
||||||
|
const assetUrns =
|
||||||
|
props.module.properties.params.assetCollectionParams?.assetUrns.filter(
|
||||||
|
(urn): urn is string => typeof urn === 'string',
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
const { entities, loading } = useGetEntities(assetUrns);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LargeModule {...props} loading={loading}>
|
||||||
|
{entities?.length === 0 ? (
|
||||||
|
<EmptyContent
|
||||||
|
icon="Stack"
|
||||||
|
title="No Assets"
|
||||||
|
description="Edit the module and add assets to see them in this list"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
entities
|
||||||
|
.filter((entity): entity is Entity => entity !== null)
|
||||||
|
.map((entity) => <EntityItem entity={entity} key={entity?.urn} />)
|
||||||
|
)}
|
||||||
|
</LargeModule>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssetCollectionModule;
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { AppliedFieldFilterUpdater, FieldToAppliedFieldFiltersMap } from '@app/searchV2/filtersV2/types';
|
||||||
|
import Filters from '@app/searchV2/searchBarV2/components/Filters';
|
||||||
|
|
||||||
|
const FiltersContainer = styled.div`
|
||||||
|
margin: -8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
searchQuery: string | undefined;
|
||||||
|
appliedFilters?: FieldToAppliedFieldFiltersMap;
|
||||||
|
updateFieldFilters?: AppliedFieldFilterUpdater;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AssetFilters = ({ searchQuery, appliedFilters, updateFieldFilters }: Props) => {
|
||||||
|
return (
|
||||||
|
<FiltersContainer>
|
||||||
|
<Filters
|
||||||
|
query={searchQuery ?? '*'}
|
||||||
|
appliedFilters={appliedFilters}
|
||||||
|
updateFieldAppliedFilters={updateFieldFilters}
|
||||||
|
/>
|
||||||
|
</FiltersContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssetFilters;
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { colors } from '@components';
|
||||||
|
import { Divider } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import SelectAssetsSection from '@app/homeV3/modules/assetCollection/SelectAssetsSection';
|
||||||
|
import SelectedAssetsSection from '@app/homeV3/modules/assetCollection/SelectedAssetsSection';
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LeftSection = styled.div`
|
||||||
|
flex: 6;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RightSection = styled.div`
|
||||||
|
flex: 4;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const VerticalDivider = styled(Divider)`
|
||||||
|
color: ${colors.gray[100]};
|
||||||
|
height: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedAssetUrns: string[];
|
||||||
|
setSelectedAssetUrns: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AssetsSection = ({ selectedAssetUrns, setSelectedAssetUrns }: Props) => {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<LeftSection>
|
||||||
|
<SelectAssetsSection
|
||||||
|
selectedAssetUrns={selectedAssetUrns}
|
||||||
|
setSelectedAssetUrns={setSelectedAssetUrns}
|
||||||
|
/>
|
||||||
|
</LeftSection>
|
||||||
|
<VerticalDivider type="vertical" />
|
||||||
|
<RightSection>
|
||||||
|
<SelectedAssetsSection
|
||||||
|
selectedAssetUrns={selectedAssetUrns}
|
||||||
|
setSelectedAssetUrns={setSelectedAssetUrns}
|
||||||
|
/>
|
||||||
|
</RightSection>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssetsSection;
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { Text } from '@components';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { EmptyContainer } from '@app/homeV3/styledComponents';
|
||||||
|
|
||||||
|
const EmptySection = () => {
|
||||||
|
return (
|
||||||
|
<EmptyContainer>
|
||||||
|
<Text color="gray">No assets found.</Text>
|
||||||
|
</EmptyContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptySection;
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import { Checkbox, Loader, SearchBar, Text } from '@components';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import EntityItem from '@app/homeV3/module/components/EntityItem';
|
||||||
|
import AssetFilters from '@app/homeV3/modules/assetCollection/AssetFilters';
|
||||||
|
import EmptySection from '@app/homeV3/modules/assetCollection/EmptySection';
|
||||||
|
import useGetAssetResults from '@app/homeV3/modules/assetCollection/useGetAssetResults';
|
||||||
|
import { LoaderContainer } from '@app/homeV3/styledComponents';
|
||||||
|
import { getEntityDisplayType } from '@app/searchV2/autoCompleteV2/utils';
|
||||||
|
import useAppliedFilters from '@app/searchV2/filtersV2/context/useAppliedFilters';
|
||||||
|
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
|
||||||
|
|
||||||
|
import { Entity } from '@types';
|
||||||
|
|
||||||
|
const AssetsSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ItemDetailsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedAssetUrns: string[];
|
||||||
|
setSelectedAssetUrns: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectAssetsSection = ({ selectedAssetUrns, setSelectedAssetUrns }: Props) => {
|
||||||
|
const entityRegistry = useEntityRegistryV2();
|
||||||
|
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string | undefined>();
|
||||||
|
const { appliedFilters, updateFieldFilters } = useAppliedFilters();
|
||||||
|
const { entities, loading } = useGetAssetResults({ searchQuery, appliedFilters });
|
||||||
|
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
|
setSearchQuery(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxChange = (urn: string) => {
|
||||||
|
setSelectedAssetUrns((prev) => (prev.includes(urn) ? prev.filter((u) => u !== urn) : [...prev, urn]));
|
||||||
|
};
|
||||||
|
|
||||||
|
const customDetailsRenderer = (entity: Entity) => {
|
||||||
|
const displayType = getEntityDisplayType(entity, entityRegistry);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemDetailsContainer>
|
||||||
|
<Text color="gray" size="sm">
|
||||||
|
{displayType}
|
||||||
|
</Text>
|
||||||
|
<Checkbox
|
||||||
|
size="xs"
|
||||||
|
isChecked={selectedAssetUrns?.includes(entity.urn)}
|
||||||
|
onCheckboxChange={() => handleCheckboxChange(entity.urn)}
|
||||||
|
/>
|
||||||
|
</ItemDetailsContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (loading) {
|
||||||
|
content = (
|
||||||
|
<LoaderContainer>
|
||||||
|
<Loader />
|
||||||
|
</LoaderContainer>
|
||||||
|
);
|
||||||
|
} else if (entities && entities.length > 0) {
|
||||||
|
content = entities?.map((entity) => (
|
||||||
|
<EntityItem
|
||||||
|
entity={entity}
|
||||||
|
key={entity.urn}
|
||||||
|
customDetailsRenderer={customDetailsRenderer}
|
||||||
|
navigateOnlyOnNameClick
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
content = <EmptySection />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AssetsSection>
|
||||||
|
<Text color="gray" weight="bold">
|
||||||
|
Search and Select Assets
|
||||||
|
</Text>
|
||||||
|
<SearchBar value={searchQuery} onChange={handleSearchChange} />
|
||||||
|
<AssetFilters
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
appliedFilters={appliedFilters}
|
||||||
|
updateFieldFilters={updateFieldFilters}
|
||||||
|
/>
|
||||||
|
{content}
|
||||||
|
</AssetsSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectAssetsSection;
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import { Text } from '@components';
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import EntityItem from '@app/homeV3/module/components/EntityItem';
|
||||||
|
import { EmptyContainer, StyledIcon } from '@app/homeV3/styledComponents';
|
||||||
|
import { useGetEntities } from '@app/sharedV2/useGetEntities';
|
||||||
|
|
||||||
|
import { Entity } from '@types';
|
||||||
|
|
||||||
|
const SelectedAssetsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedAssetUrns: string[];
|
||||||
|
setSelectedAssetUrns: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectedAssetsSection = ({ selectedAssetUrns, setSelectedAssetUrns }: Props) => {
|
||||||
|
const { entities, loading } = useGetEntities(selectedAssetUrns);
|
||||||
|
|
||||||
|
const handleRemoveAsset = (entity: Entity) => {
|
||||||
|
const newUrns = selectedAssetUrns.filter((urn) => !(entity.urn === urn));
|
||||||
|
setSelectedAssetUrns(newUrns);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRemoveAsset = (entity: Entity) => {
|
||||||
|
return (
|
||||||
|
<StyledIcon
|
||||||
|
icon="X"
|
||||||
|
source="phosphor"
|
||||||
|
color="gray"
|
||||||
|
size="md"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRemoveAsset(entity);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (entities && entities.length > 0) {
|
||||||
|
content = entities.map((entity) => (
|
||||||
|
<EntityItem
|
||||||
|
entity={entity}
|
||||||
|
key={entity.urn}
|
||||||
|
customDetailsRenderer={renderRemoveAsset}
|
||||||
|
navigateOnlyOnNameClick
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
} else if (!loading && entities.length === 0) {
|
||||||
|
content = (
|
||||||
|
<EmptyContainer>
|
||||||
|
<Text color="gray">No assets selected.</Text>
|
||||||
|
</EmptyContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectedAssetsContainer>
|
||||||
|
<Text color="gray" weight="bold">
|
||||||
|
Selected Assets
|
||||||
|
</Text>
|
||||||
|
{content}
|
||||||
|
</SelectedAssetsContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectedAssetsSection;
|
||||||
@ -0,0 +1,175 @@
|
|||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import useGetAssetResults from '@app/homeV3/modules/assetCollection/useGetAssetResults';
|
||||||
|
import { convertFiltersMapToFilters } from '@app/searchV2/filtersV2/utils';
|
||||||
|
import { UnionType } from '@app/searchV2/utils/constants';
|
||||||
|
import { generateOrFilters } from '@app/searchV2/utils/generateOrFilters';
|
||||||
|
|
||||||
|
import { useGetSearchResultsForMultipleQuery } from '@graphql/search.generated';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('@app/searchV2/filtersV2/utils', () => ({
|
||||||
|
convertFiltersMapToFilters: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@app/searchV2/utils/generateOrFilters', () => ({
|
||||||
|
generateOrFilters: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@graphql/search.generated', () => ({
|
||||||
|
useGetSearchResultsForMultipleQuery: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useGetAssetResults', () => {
|
||||||
|
const mockAppliedFilters = new Map();
|
||||||
|
const mockFilters = [{ field: 'testField', value: 'testValue' }];
|
||||||
|
const mockOrFilters = [{ or: 'filters' }];
|
||||||
|
const mockData = {
|
||||||
|
searchAcrossEntities: {
|
||||||
|
searchResults: [{ entity: { urn: 'urn:li:entity:1' } }, { entity: { urn: 'urn:li:entity:2' } }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertFiltersMapToFiltersMock = convertFiltersMapToFilters as unknown as Mock;
|
||||||
|
const generateOrFiltersMock = generateOrFilters as unknown as Mock;
|
||||||
|
const useGetSearchResultsForMultipleQueryMock = useGetSearchResultsForMultipleQuery as unknown as Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
convertFiltersMapToFiltersMock.mockReturnValue(mockFilters);
|
||||||
|
generateOrFiltersMock.mockReturnValue(mockOrFilters);
|
||||||
|
useGetSearchResultsForMultipleQueryMock.mockReturnValue({ data: mockData, loading: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call convertFiltersMapToFilters with appliedFilters', () => {
|
||||||
|
renderHook(() => useGetAssetResults({ searchQuery: 'query', appliedFilters: mockAppliedFilters }));
|
||||||
|
expect(convertFiltersMapToFiltersMock).toHaveBeenCalledWith(mockAppliedFilters);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call generateOrFilters with UnionType.AND and filters', () => {
|
||||||
|
renderHook(() => useGetAssetResults({ searchQuery: 'query', appliedFilters: mockAppliedFilters }));
|
||||||
|
expect(generateOrFiltersMock).toHaveBeenCalledWith(UnionType.AND, mockFilters);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call useGetSearchResultsForMultipleQuery with correct variables', () => {
|
||||||
|
const searchQuery = 'query';
|
||||||
|
renderHook(() => useGetAssetResults({ searchQuery, appliedFilters: mockAppliedFilters }));
|
||||||
|
expect(useGetSearchResultsForMultipleQueryMock).toHaveBeenCalledWith({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
query: searchQuery,
|
||||||
|
start: 0,
|
||||||
|
count: 10,
|
||||||
|
orFilters: mockOrFilters,
|
||||||
|
searchFlags: { skipCache: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use * as query if searchQuery is undefined', () => {
|
||||||
|
renderHook(() => useGetAssetResults({ searchQuery: undefined, appliedFilters: mockAppliedFilters }));
|
||||||
|
expect(useGetSearchResultsForMultipleQueryMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: expect.objectContaining({ input: expect.objectContaining({ query: '*' }) }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return entities from data', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useGetAssetResults({ searchQuery: 'query', appliedFilters: mockAppliedFilters }),
|
||||||
|
);
|
||||||
|
expect(result.current.entities).toEqual(mockData.searchAcrossEntities.searchResults.map((res) => res.entity));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array if data is undefined', () => {
|
||||||
|
useGetSearchResultsForMultipleQueryMock.mockReturnValue({ data: undefined, loading: false });
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useGetAssetResults({ searchQuery: 'query', appliedFilters: mockAppliedFilters }),
|
||||||
|
);
|
||||||
|
expect(result.current.entities).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array if searchAcrossEntities is missing', () => {
|
||||||
|
useGetSearchResultsForMultipleQueryMock.mockReturnValue({ data: {}, loading: false });
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useGetAssetResults({ searchQuery: 'foo', appliedFilters: mockAppliedFilters }),
|
||||||
|
);
|
||||||
|
expect(result.current.entities).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array if searchResults is missing', () => {
|
||||||
|
useGetSearchResultsForMultipleQueryMock.mockReturnValue({ data: { searchAcrossEntities: {} }, loading: false });
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useGetAssetResults({ searchQuery: 'foo', appliedFilters: mockAppliedFilters }),
|
||||||
|
);
|
||||||
|
expect(result.current.entities).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use loading from useGetSearchResultsForMultipleQuery', () => {
|
||||||
|
useGetSearchResultsForMultipleQueryMock.mockReturnValue({ data: mockData, loading: true });
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useGetAssetResults({ searchQuery: 'bar', appliedFilters: mockAppliedFilters }),
|
||||||
|
);
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty appliedFilters', () => {
|
||||||
|
convertFiltersMapToFiltersMock.mockReturnValue([]);
|
||||||
|
renderHook(() => useGetAssetResults({ searchQuery: 'foo', appliedFilters: new Map() }));
|
||||||
|
expect(generateOrFiltersMock).toHaveBeenCalledWith(UnionType.AND, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle appliedFilters with multiple fields', () => {
|
||||||
|
const multiFilters = [
|
||||||
|
{ field: 'testField', value: 'testValue' },
|
||||||
|
{ field: 'anotherField', value: 'anotherValue' },
|
||||||
|
];
|
||||||
|
convertFiltersMapToFiltersMock.mockReturnValue(multiFilters);
|
||||||
|
renderHook(() => useGetAssetResults({ searchQuery: 'foo', appliedFilters: new Map([['k', { filters: [] }]]) }));
|
||||||
|
expect(generateOrFiltersMock).toHaveBeenCalledWith(UnionType.AND, multiFilters);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass through searchFlags.skipCache', () => {
|
||||||
|
renderHook(() => useGetAssetResults({ searchQuery: 'baz', appliedFilters: mockAppliedFilters }));
|
||||||
|
const call = useGetSearchResultsForMultipleQueryMock.mock.calls[0][0];
|
||||||
|
expect(call.variables.input.searchFlags.skipCache).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should memoize filter conversion results (filters object)', () => {
|
||||||
|
// Set up for checking memoization via rerender
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ searchQuery, appliedFilters }) => useGetAssetResults({ searchQuery, appliedFilters }),
|
||||||
|
{
|
||||||
|
initialProps: { searchQuery: 'foo', appliedFilters: mockAppliedFilters },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
rerender({ searchQuery: 'foo', appliedFilters: mockAppliedFilters });
|
||||||
|
// convertFiltersMapToFilters should not be called additional times (React useMemo prevents it)
|
||||||
|
expect(convertFiltersMapToFiltersMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle when searchResults is not an array (null)', () => {
|
||||||
|
useGetSearchResultsForMultipleQueryMock.mockReturnValue({
|
||||||
|
data: { searchAcrossEntities: { searchResults: null } },
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useGetAssetResults({ searchQuery: 'result', appliedFilters: mockAppliedFilters }),
|
||||||
|
);
|
||||||
|
expect(result.current.entities).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use empty array for entities if searchResults is empty', () => {
|
||||||
|
useGetSearchResultsForMultipleQueryMock.mockReturnValue({
|
||||||
|
data: { searchAcrossEntities: { searchResults: [] } },
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useGetAssetResults({ searchQuery: 'result', appliedFilters: mockAppliedFilters }),
|
||||||
|
);
|
||||||
|
expect(result.current.entities).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export type FormValues = {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { FieldToAppliedFieldFiltersMap } from '@app/searchV2/filtersV2/types';
|
||||||
|
import { convertFiltersMapToFilters } from '@app/searchV2/filtersV2/utils';
|
||||||
|
import { UnionType } from '@app/searchV2/utils/constants';
|
||||||
|
import { generateOrFilters } from '@app/searchV2/utils/generateOrFilters';
|
||||||
|
|
||||||
|
import { useGetSearchResultsForMultipleQuery } from '@graphql/search.generated';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
searchQuery: string | undefined;
|
||||||
|
appliedFilters?: FieldToAppliedFieldFiltersMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useGetAssetResults({ searchQuery, appliedFilters }: Props) {
|
||||||
|
const filters = useMemo(() => convertFiltersMapToFilters(appliedFilters), [appliedFilters]);
|
||||||
|
const orFilters = generateOrFilters(UnionType.AND, filters);
|
||||||
|
|
||||||
|
const { data, loading } = useGetSearchResultsForMultipleQuery({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
query: searchQuery || '*',
|
||||||
|
start: 0,
|
||||||
|
count: 10,
|
||||||
|
orFilters,
|
||||||
|
searchFlags: {
|
||||||
|
skipCache: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const entities = data?.searchAcrossEntities?.searchResults?.map((res) => res.entity) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
entities,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -49,16 +49,24 @@ export const DEFAULT_MODULE_LINK: ModuleInfo = {
|
|||||||
key: 'default_module_quick_link',
|
key: 'default_module_quick_link',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CUSTOM_LARGE_MODULE_ASSET_COLLECTION: ModuleInfo = {
|
||||||
|
type: DataHubPageModuleType.AssetCollection,
|
||||||
|
name: 'Asset Collection',
|
||||||
|
description: MODULE_TYPE_TO_DESCRIPTION.get(DataHubPageModuleType.AssetCollection),
|
||||||
|
icon: MODULE_TYPE_TO_ICON.get(DataHubPageModuleType.AssetCollection) ?? DEFAULT_MODULE_ICON,
|
||||||
|
key: 'custom_large_module_asset_collection',
|
||||||
|
};
|
||||||
|
|
||||||
export const DEFAULT_MODULES: ModuleInfo[] = [
|
export const DEFAULT_MODULES: ModuleInfo[] = [
|
||||||
DEFAULT_MODULE_YOUR_ASSETS,
|
DEFAULT_MODULE_YOUR_ASSETS,
|
||||||
DEFAULT_MODULE_TOP_DOMAINS,
|
DEFAULT_MODULE_TOP_DOMAINS,
|
||||||
|
CUSTOM_LARGE_MODULE_ASSET_COLLECTION,
|
||||||
// Links isn't supported yet
|
// Links isn't supported yet
|
||||||
// DEFAULT_MODULE_LINK,
|
// DEFAULT_MODULE_LINK,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ADD_MODULE_MENU_SECTION_CUSTOM_MODULE_TYPES: DataHubPageModuleType[] = [
|
export const ADD_MODULE_MENU_SECTION_CUSTOM_MODULE_TYPES: DataHubPageModuleType[] = [
|
||||||
DataHubPageModuleType.Link,
|
DataHubPageModuleType.Link,
|
||||||
DataHubPageModuleType.AssetCollection,
|
|
||||||
DataHubPageModuleType.RichText,
|
DataHubPageModuleType.RichText,
|
||||||
DataHubPageModuleType.Hierarchy,
|
DataHubPageModuleType.Hierarchy,
|
||||||
];
|
];
|
||||||
@ -66,4 +74,10 @@ export const ADD_MODULE_MENU_SECTION_CUSTOM_MODULE_TYPES: DataHubPageModuleType[
|
|||||||
export const ADD_MODULE_MENU_SECTION_CUSTOM_LARGE_MODULE_TYPES: DataHubPageModuleType[] = [
|
export const ADD_MODULE_MENU_SECTION_CUSTOM_LARGE_MODULE_TYPES: DataHubPageModuleType[] = [
|
||||||
DataHubPageModuleType.Domains,
|
DataHubPageModuleType.Domains,
|
||||||
DataHubPageModuleType.OwnedAssets,
|
DataHubPageModuleType.OwnedAssets,
|
||||||
|
DataHubPageModuleType.AssetCollection,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_GLOBAL_MODULE_TYPES: DataHubPageModuleType[] = [
|
||||||
|
DataHubPageModuleType.OwnedAssets,
|
||||||
|
DataHubPageModuleType.Domains,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -59,3 +59,17 @@ export const StyledIcon = styled(Icon)`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const LoaderContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 200px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EmptyContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
height: 50%;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|||||||
@ -3,11 +3,14 @@ import React, { useMemo } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
|
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
|
||||||
|
import ModuleModalMapper from '@app/homeV3/moduleModals/ModuleModalMapper';
|
||||||
import useModulesAvailableToAdd from '@app/homeV3/modules/hooks/useModulesAvailableToAdd';
|
import useModulesAvailableToAdd from '@app/homeV3/modules/hooks/useModulesAvailableToAdd';
|
||||||
import AddModuleButton from '@app/homeV3/template/components/AddModuleButton';
|
import AddModuleButton from '@app/homeV3/template/components/AddModuleButton';
|
||||||
import TemplateRow from '@app/homeV3/templateRow/TemplateRow';
|
import TemplateRow from '@app/homeV3/templateRow/TemplateRow';
|
||||||
import { wrapRows } from '@app/homeV3/templateRow/utils';
|
import { wrapRows } from '@app/homeV3/templateRow/utils';
|
||||||
|
|
||||||
|
import { DataHubPageTemplateRow } from '@types';
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -25,7 +28,10 @@ interface Props {
|
|||||||
|
|
||||||
export default function Template({ className }: Props) {
|
export default function Template({ className }: Props) {
|
||||||
const { template } = usePageTemplateContext();
|
const { template } = usePageTemplateContext();
|
||||||
const rows = useMemo(() => template?.properties?.rows ?? [], [template?.properties?.rows]);
|
const rows = useMemo(
|
||||||
|
() => (template?.properties?.rows ?? []) as DataHubPageTemplateRow[],
|
||||||
|
[template?.properties?.rows],
|
||||||
|
);
|
||||||
const hasRows = useMemo(() => !!rows.length, [rows.length]);
|
const hasRows = useMemo(() => !!rows.length, [rows.length]);
|
||||||
const wrappedRows = useMemo(() => wrapRows(rows), [rows]);
|
const wrappedRows = useMemo(() => wrapRows(rows), [rows]);
|
||||||
|
|
||||||
@ -42,6 +48,7 @@ export default function Template({ className }: Props) {
|
|||||||
$hasRows={hasRows}
|
$hasRows={hasRows}
|
||||||
modulesAvailableToAdd={modulesAvailableToAdd}
|
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||||
/>
|
/>
|
||||||
|
<ModuleModalMapper />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,7 +61,9 @@ export default function AddModuleButton({ orientation, modulesAvailableToAdd, cl
|
|||||||
rowSide,
|
rowSide,
|
||||||
};
|
};
|
||||||
|
|
||||||
const menu = useAddModuleMenu(modulesAvailableToAdd, position, () => setIsOpened(false));
|
const closeMenu = () => setIsOpened(false);
|
||||||
|
|
||||||
|
const menu = useAddModuleMenu(modulesAvailableToAdd, position, closeMenu);
|
||||||
|
|
||||||
const onClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
const onClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
// FYI: Antd can open dropdown in the cursor's position only for contextMenu trigger
|
// FYI: Antd can open dropdown in the cursor's position only for contextMenu trigger
|
||||||
|
|||||||
@ -11,9 +11,16 @@ import { DataHubPageModuleType, EntityType, PageModuleScope } from '@types';
|
|||||||
|
|
||||||
// Mock the PageTemplateContext
|
// Mock the PageTemplateContext
|
||||||
const mockAddModule = vi.fn();
|
const mockAddModule = vi.fn();
|
||||||
|
const mockOpenModal = vi.fn();
|
||||||
vi.mock('@app/homeV3/context/PageTemplateContext', () => ({
|
vi.mock('@app/homeV3/context/PageTemplateContext', () => ({
|
||||||
usePageTemplateContext: () => ({
|
usePageTemplateContext: () => ({
|
||||||
addModule: mockAddModule,
|
addModule: mockAddModule,
|
||||||
|
moduleModalState: {
|
||||||
|
open: mockOpenModal,
|
||||||
|
close: vi.fn(),
|
||||||
|
isOpen: false,
|
||||||
|
isEditing: false,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -58,6 +65,11 @@ describe('useAddModuleMenu', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getChildren(item: any): any[] {
|
||||||
|
if (item && 'children' in item && Array.isArray(item.children)) return item.children;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
it('should return menu items with hardcoded custom modules', () => {
|
it('should return menu items with hardcoded custom modules', () => {
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAddModuleMenu(
|
useAddModuleMenu(
|
||||||
@ -86,7 +98,7 @@ describe('useAddModuleMenu', () => {
|
|||||||
// Check Custom Large group
|
// Check Custom Large group
|
||||||
expect(items?.[1]).toHaveProperty('key', 'customLargeModulesGroup');
|
expect(items?.[1]).toHaveProperty('key', 'customLargeModulesGroup');
|
||||||
// @ts-expect-error SubMenuItem should have children
|
// @ts-expect-error SubMenuItem should have children
|
||||||
expect(items?.[1]?.children).toHaveLength(2);
|
expect(items?.[1]?.children).toHaveLength(3);
|
||||||
// @ts-expect-error SubMenuItem should have children
|
// @ts-expect-error SubMenuItem should have children
|
||||||
expect(items?.[1]?.children?.[0]).toHaveProperty('key', 'your-assets');
|
expect(items?.[1]?.children?.[0]).toHaveProperty('key', 'your-assets');
|
||||||
// @ts-expect-error SubMenuItem should have children
|
// @ts-expect-error SubMenuItem should have children
|
||||||
@ -168,4 +180,40 @@ describe('useAddModuleMenu', () => {
|
|||||||
expect(adminGroup).toHaveProperty('expandIcon');
|
expect(adminGroup).toHaveProperty('expandIcon');
|
||||||
expect(adminGroup).toHaveProperty('popupClassName');
|
expect(adminGroup).toHaveProperty('popupClassName');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should open module modal when Asset Collection is clicked', () => {
|
||||||
|
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockPosition, mockCloseMenu));
|
||||||
|
const customLargeChildren = getChildren(result.current.items?.[1]);
|
||||||
|
// Third child is Asset Collection
|
||||||
|
const assetCollectionItem = customLargeChildren[2];
|
||||||
|
assetCollectionItem.onClick?.({} as any);
|
||||||
|
|
||||||
|
expect(mockOpenModal).toHaveBeenCalledWith(DataHubPageModuleType.AssetCollection, mockPosition);
|
||||||
|
expect(mockCloseMenu).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call addModule when Asset Collection is clicked', () => {
|
||||||
|
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockPosition, mockCloseMenu));
|
||||||
|
const customLargeChildren = getChildren(result.current.items?.[1]);
|
||||||
|
const assetCollectionItem = customLargeChildren[2];
|
||||||
|
assetCollectionItem.onClick?.({} as any);
|
||||||
|
|
||||||
|
expect(mockAddModule).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
module: expect.objectContaining({
|
||||||
|
urn: 'urn:li:dataHubPageModule:asset-collection',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
mockPosition,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not open modal when Your Assets is clicked', () => {
|
||||||
|
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockPosition, mockCloseMenu));
|
||||||
|
const customLargeChildren = getChildren(result.current.items?.[1]);
|
||||||
|
const yourAssetsItem = customLargeChildren[0];
|
||||||
|
yourAssetsItem.onClick?.({} as any);
|
||||||
|
|
||||||
|
expect(mockOpenModal).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -40,8 +40,11 @@ export default function useAddModuleMenu(
|
|||||||
modulesAvailableToAdd: ModulesAvailableToAdd,
|
modulesAvailableToAdd: ModulesAvailableToAdd,
|
||||||
position: ModulePositionInput,
|
position: ModulePositionInput,
|
||||||
closeMenu: () => void,
|
closeMenu: () => void,
|
||||||
): MenuProps {
|
) {
|
||||||
const { addModule } = usePageTemplateContext();
|
const {
|
||||||
|
addModule,
|
||||||
|
moduleModalState: { open: openModal },
|
||||||
|
} = usePageTemplateContext();
|
||||||
|
|
||||||
const handleAddExistingModule = useCallback(
|
const handleAddExistingModule = useCallback(
|
||||||
(module: PageModuleFragment) => {
|
(module: PageModuleFragment) => {
|
||||||
@ -54,20 +57,15 @@ export default function useAddModuleMenu(
|
|||||||
[addModule, position, closeMenu],
|
[addModule, position, closeMenu],
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: use this commented out code later once we implement creating new modules
|
const handleOpenCreateModuleModal = useCallback(
|
||||||
// const handleCreateNewModule = useCallback(
|
(type: DataHubPageModuleType) => {
|
||||||
// (type: DataHubPageModuleType, name: string) => {
|
openModal(type, position);
|
||||||
// createModule({
|
closeMenu();
|
||||||
// name,
|
},
|
||||||
// type,
|
[openModal, position, closeMenu],
|
||||||
// position,
|
);
|
||||||
// });
|
|
||||||
// closeMenu();
|
|
||||||
// },
|
|
||||||
// [createModule, position, closeMenu],
|
|
||||||
// );
|
|
||||||
|
|
||||||
return useMemo(() => {
|
const menu = useMemo(() => {
|
||||||
const items: MenuProps['items'] = [];
|
const items: MenuProps['items'] = [];
|
||||||
|
|
||||||
const quickLink = {
|
const quickLink = {
|
||||||
@ -113,11 +111,26 @@ export default function useAddModuleMenu(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const assetCollection = {
|
||||||
|
title: 'Asset Collection',
|
||||||
|
key: 'asset-collection',
|
||||||
|
label: (
|
||||||
|
<MenuItem
|
||||||
|
description="A curated list of assets of your choosing"
|
||||||
|
title="Asset Collection"
|
||||||
|
icon="Stack"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
onClick: () => {
|
||||||
|
handleOpenCreateModuleModal(DataHubPageModuleType.AssetCollection);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
key: 'customLargeModulesGroup',
|
key: 'customLargeModulesGroup',
|
||||||
label: <GroupItem title="Custom Large" />,
|
label: <GroupItem title="Custom Large" />,
|
||||||
type: 'group',
|
type: 'group',
|
||||||
children: [yourAssets, domains],
|
children: [yourAssets, domains, assetCollection],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add admin created modules if available
|
// Add admin created modules if available
|
||||||
@ -147,5 +160,7 @@ export default function useAddModuleMenu(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { items };
|
return { items };
|
||||||
}, [modulesAvailableToAdd, handleAddExistingModule]);
|
}, [modulesAvailableToAdd.adminCreatedModules, handleAddExistingModule, handleOpenCreateModuleModal]);
|
||||||
|
|
||||||
|
return menu;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import styled, { useTheme } from 'styled-components';
|
import styled, { useTheme } from 'styled-components';
|
||||||
|
|
||||||
import { getColor } from '@components/theme/utils';
|
import { getColor } from '@components/theme/utils';
|
||||||
@ -15,26 +16,43 @@ import { Text } from '@src/alchemy-components';
|
|||||||
import { useEntityRegistryV2 } from '@src/app/useEntityRegistry';
|
import { useEntityRegistryV2 } from '@src/app/useEntityRegistry';
|
||||||
import { Entity, MatchedField } from '@src/types.generated';
|
import { Entity, MatchedField } from '@src/types.generated';
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div<{
|
||||||
|
$navigateOnlyOnNameClick?: boolean;
|
||||||
|
}>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 13px 8px 8px;
|
padding: 8px 13px 8px 8px;
|
||||||
|
|
||||||
:hover {
|
${(props) =>
|
||||||
cursor: pointer;
|
!props.$navigateOnlyOnNameClick &&
|
||||||
}
|
`
|
||||||
|
:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// FYI: this hovering dependent on Container can't be applied by condition inside of styled component
|
// FYI: this hovering dependent on Container can't be applied by condition inside of styled component
|
||||||
// so we have this separated version with hover
|
// so we have this separated version with hover
|
||||||
const DisplayNameWithHover = styled(DisplayName)<{ $decorationColor?: string }>`
|
|
||||||
|
// On container hover
|
||||||
|
const DisplayNameHoverFromContainer = styled(DisplayName)<{ $decorationColor?: string }>`
|
||||||
${Container}:hover & {
|
${Container}:hover & {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
${(props) => props.$decorationColor && `text-decoration-color: ${props.$decorationColor};`}
|
${(props) => props.$decorationColor && `text-decoration-color: ${props.$decorationColor};`}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// On self (name) hover only
|
||||||
|
const DisplayNameHoverFromSelf = styled(DisplayName)<{ $decorationColor?: string }>`
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
${(props) => props.$decorationColor && `text-decoration-color: ${props.$decorationColor};`}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const DisplayNameWrapper = styled.div`
|
const DisplayNameWrapper = styled.div`
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
@ -80,6 +98,7 @@ interface EntityAutocompleteItemProps {
|
|||||||
matchedFields?: MatchedField[];
|
matchedFields?: MatchedField[];
|
||||||
variant?: EntityItemVariant;
|
variant?: EntityItemVariant;
|
||||||
customDetailsRenderer?: (entity: Entity) => void;
|
customDetailsRenderer?: (entity: Entity) => void;
|
||||||
|
navigateOnlyOnNameClick?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutoCompleteEntityItem({
|
export default function AutoCompleteEntityItem({
|
||||||
@ -89,6 +108,7 @@ export default function AutoCompleteEntityItem({
|
|||||||
matchedFields,
|
matchedFields,
|
||||||
variant,
|
variant,
|
||||||
customDetailsRenderer,
|
customDetailsRenderer,
|
||||||
|
navigateOnlyOnNameClick,
|
||||||
}: EntityAutocompleteItemProps) {
|
}: EntityAutocompleteItemProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const entityRegistry = useEntityRegistryV2();
|
const entityRegistry = useEntityRegistryV2();
|
||||||
@ -96,8 +116,34 @@ export default function AutoCompleteEntityItem({
|
|||||||
const displayType = getEntityDisplayType(entity, entityRegistry);
|
const displayType = getEntityDisplayType(entity, entityRegistry);
|
||||||
const variantProps = VARIANT_STYLES.get(variant ?? 'default');
|
const variantProps = VARIANT_STYLES.get(variant ?? 'default');
|
||||||
|
|
||||||
|
const DisplayNameHoverComponent = navigateOnlyOnNameClick
|
||||||
|
? DisplayNameHoverFromSelf
|
||||||
|
: DisplayNameHoverFromContainer;
|
||||||
|
|
||||||
|
const displayNameContent = variantProps?.nameCanBeHovered ? (
|
||||||
|
<Link to={entityRegistry.getEntityUrl(entity.type, entity.urn)}>
|
||||||
|
<DisplayNameHoverComponent
|
||||||
|
displayName={displayName}
|
||||||
|
highlight={query}
|
||||||
|
color={variantProps?.nameColor}
|
||||||
|
colorLevel={variantProps?.nameColorLevel}
|
||||||
|
weight={variantProps?.nameWeight}
|
||||||
|
$decorationColor={getColor(variantProps?.nameColor, variantProps?.nameColorLevel, theme)}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<DisplayName
|
||||||
|
displayName={displayName}
|
||||||
|
highlight={query}
|
||||||
|
color={variantProps?.nameColor}
|
||||||
|
colorLevel={variantProps?.nameColorLevel}
|
||||||
|
weight={variantProps?.nameWeight}
|
||||||
|
showNameTooltipIfTruncated
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container $navigateOnlyOnNameClick={navigateOnlyOnNameClick}>
|
||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
<IconContainer $variant={variant}>
|
<IconContainer $variant={variant}>
|
||||||
<EntityIcon entity={entity} siblings={siblings} />
|
<EntityIcon entity={entity} siblings={siblings} />
|
||||||
@ -110,31 +156,7 @@ export default function AutoCompleteEntityItem({
|
|||||||
showArrow={false}
|
showArrow={false}
|
||||||
canOpen={variantProps?.showEntityPopover}
|
canOpen={variantProps?.showEntityPopover}
|
||||||
>
|
>
|
||||||
<DisplayNameWrapper>
|
<DisplayNameWrapper>{displayNameContent}</DisplayNameWrapper>
|
||||||
{variantProps?.nameCanBeHovered ? (
|
|
||||||
<DisplayNameWithHover
|
|
||||||
displayName={displayName}
|
|
||||||
highlight={query}
|
|
||||||
color={variantProps?.nameColor}
|
|
||||||
colorLevel={variantProps?.nameColorLevel}
|
|
||||||
weight={variantProps?.nameWeight}
|
|
||||||
$decorationColor={getColor(
|
|
||||||
variantProps?.nameColor,
|
|
||||||
variantProps?.nameColorLevel,
|
|
||||||
theme,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DisplayName
|
|
||||||
displayName={displayName}
|
|
||||||
highlight={query}
|
|
||||||
color={variantProps?.nameColor}
|
|
||||||
colorLevel={variantProps?.nameColorLevel}
|
|
||||||
weight={variantProps?.nameWeight}
|
|
||||||
showNameTooltipIfTruncated
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DisplayNameWrapper>
|
|
||||||
</HoverEntityTooltip>
|
</HoverEntityTooltip>
|
||||||
|
|
||||||
<EntitySubtitle
|
<EntitySubtitle
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
} from '@app/searchV2/utils/constants';
|
} from '@app/searchV2/utils/constants';
|
||||||
import { FacetMetadata } from '@src/types.generated';
|
import { FacetMetadata } from '@src/types.generated';
|
||||||
|
|
||||||
const FILTER_FIELDS = [
|
export const FILTER_FIELDS = [
|
||||||
PLATFORM_FILTER_NAME,
|
PLATFORM_FILTER_NAME,
|
||||||
ENTITY_SUB_TYPE_FILTER_NAME,
|
ENTITY_SUB_TYPE_FILTER_NAME,
|
||||||
OWNERS_FILTER_NAME,
|
OWNERS_FILTER_NAME,
|
||||||
|
|||||||
@ -0,0 +1,120 @@
|
|||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { useGetEntities } from '@app/sharedV2/useGetEntities';
|
||||||
|
|
||||||
|
import { useGetEntitiesQuery } from '@graphql/entity.generated';
|
||||||
|
import { type Entity, EntityType } from '@types';
|
||||||
|
|
||||||
|
// Mocking the query hook
|
||||||
|
vi.mock('@graphql/entity.generated', () => ({
|
||||||
|
useGetEntitiesQuery: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const useGetEntitiesQueryMock = useGetEntitiesQuery as unknown as ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
|
const VALID_URNS = ['urn:li:dataset:123', 'urn:li:chart:456'];
|
||||||
|
const MIXED_URNS = ['notAUrn', 'urn:li:dataset:1', 'foo', 'urn:li:chart:2'];
|
||||||
|
const INVALID_URNS = ['foo', 'bar', 'notUrn'];
|
||||||
|
|
||||||
|
const MOCK_ENTITIES: Entity[] = [
|
||||||
|
{ urn: 'urn:li:dataset:123', type: EntityType.Dataset },
|
||||||
|
{ urn: 'urn:li:chart:456', type: EntityType.Chart },
|
||||||
|
];
|
||||||
|
const PARTIAL_ENTITIES: Entity[] = [{ urn: 'urn:li:chart:2', type: EntityType.Chart }];
|
||||||
|
|
||||||
|
describe('useGetEntities', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter for valid urns and call useGetEntitiesQuery', () => {
|
||||||
|
useGetEntitiesQueryMock.mockReturnValue({ data: { entities: MOCK_ENTITIES }, loading: false });
|
||||||
|
renderHook(() => useGetEntities([...VALID_URNS]));
|
||||||
|
expect(useGetEntitiesQueryMock).toHaveBeenCalledWith({
|
||||||
|
variables: { urns: VALID_URNS },
|
||||||
|
skip: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out invalid urns', () => {
|
||||||
|
useGetEntitiesQueryMock.mockReturnValue({ data: { entities: PARTIAL_ENTITIES }, loading: false });
|
||||||
|
renderHook(() => useGetEntities([...MIXED_URNS]));
|
||||||
|
expect(useGetEntitiesQueryMock).toHaveBeenCalledWith({
|
||||||
|
variables: { urns: ['urn:li:dataset:1', 'urn:li:chart:2'] },
|
||||||
|
skip: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip query if no valid urns are provided', () => {
|
||||||
|
renderHook(() => useGetEntities([...INVALID_URNS]));
|
||||||
|
expect(useGetEntitiesQueryMock).toHaveBeenCalledWith({
|
||||||
|
variables: { urns: [] },
|
||||||
|
skip: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip query if urns array is empty', () => {
|
||||||
|
renderHook(() => useGetEntities([]));
|
||||||
|
expect(useGetEntitiesQueryMock).toHaveBeenCalledWith({
|
||||||
|
variables: { urns: [] },
|
||||||
|
skip: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return loading=true when query is loading', () => {
|
||||||
|
useGetEntitiesQueryMock.mockReturnValue({ data: undefined, loading: true });
|
||||||
|
const { result } = renderHook(() => useGetEntities([...VALID_URNS]));
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
expect(result.current.entities).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return entities from data', () => {
|
||||||
|
useGetEntitiesQueryMock.mockReturnValue({ data: { entities: MOCK_ENTITIES }, loading: false });
|
||||||
|
const { result } = renderHook(() => useGetEntities([...VALID_URNS]));
|
||||||
|
expect(result.current.entities).toEqual(MOCK_ENTITIES);
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array of entities if data is undefined', () => {
|
||||||
|
useGetEntitiesQueryMock.mockReturnValue({ data: undefined, loading: false });
|
||||||
|
const { result } = renderHook(() => useGetEntities([...VALID_URNS]));
|
||||||
|
expect(result.current.entities).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array if data.entities is missing', () => {
|
||||||
|
useGetEntitiesQueryMock.mockReturnValue({ data: {}, loading: false });
|
||||||
|
const { result } = renderHook(() => useGetEntities([...VALID_URNS]));
|
||||||
|
expect(result.current.entities).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array if data.entities is null', () => {
|
||||||
|
useGetEntitiesQueryMock.mockReturnValue({ data: { entities: null }, loading: false });
|
||||||
|
const { result } = renderHook(() => useGetEntities([...VALID_URNS]));
|
||||||
|
expect(result.current.entities).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default to empty array if data.entities is not an array', () => {
|
||||||
|
useGetEntitiesQueryMock.mockReturnValue({ data: { entities: 123 }, loading: false });
|
||||||
|
const { result } = renderHook(() => useGetEntities([...VALID_URNS]));
|
||||||
|
expect(Array.isArray(result.current.entities)).toBe(true);
|
||||||
|
expect(result.current.entities.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call with skip: true if all urns are filtered out (not matching urn:li:)', () => {
|
||||||
|
renderHook(() => useGetEntities(['foo', 'bar']));
|
||||||
|
expect(useGetEntitiesQueryMock).toHaveBeenCalledWith({
|
||||||
|
variables: { urns: [] },
|
||||||
|
skip: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed scenarios: valid, invalid, undefined', () => {
|
||||||
|
useGetEntitiesQueryMock.mockReturnValue({ data: { entities: PARTIAL_ENTITIES }, loading: false });
|
||||||
|
renderHook(() => useGetEntities(['urn:li:chart:2', undefined as any, 'bad', null as any]));
|
||||||
|
expect(useGetEntitiesQueryMock).toHaveBeenCalledWith({
|
||||||
|
variables: { urns: ['urn:li:chart:2'] },
|
||||||
|
skip: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -13,7 +13,7 @@ export function usePropagationDetails(sourceDetail?: StringMapEntry[] | null) {
|
|||||||
const originEntityUrn = sourceDetail?.find((mapEntry) => mapEntry.key === 'origin')?.value || '';
|
const originEntityUrn = sourceDetail?.find((mapEntry) => mapEntry.key === 'origin')?.value || '';
|
||||||
const viaEntityUrn = sourceDetail?.find((mapEntry) => mapEntry.key === 'via')?.value || '';
|
const viaEntityUrn = sourceDetail?.find((mapEntry) => mapEntry.key === 'via')?.value || '';
|
||||||
|
|
||||||
const entities = useGetEntities([originEntityUrn, viaEntityUrn]);
|
const { entities } = useGetEntities([originEntityUrn, viaEntityUrn]);
|
||||||
const originEntity = entities.find((e) => e.urn === originEntityUrn);
|
const originEntity = entities.find((e) => e.urn === originEntityUrn);
|
||||||
const viaEntity = entities.find((e) => e.urn === viaEntityUrn);
|
const viaEntity = entities.find((e) => e.urn === viaEntityUrn);
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,13 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { useGetEntitiesQuery } from '@graphql/entity.generated';
|
import { useGetEntitiesQuery } from '@graphql/entity.generated';
|
||||||
import { Entity } from '@types';
|
import { Entity } from '@types';
|
||||||
|
|
||||||
export function useGetEntities(urns: string[]): Entity[] {
|
export function useGetEntities(urns: string[]): {
|
||||||
const [verifiedUrns, setVerifiedUrns] = useState<string[]>([]);
|
entities: Entity[];
|
||||||
|
loading: boolean;
|
||||||
|
} {
|
||||||
|
const verifiedUrns = urns.filter((urn) => typeof urn === 'string' && urn.startsWith('urn:li:'));
|
||||||
|
|
||||||
useEffect(() => {
|
const { data, loading } = useGetEntitiesQuery({ variables: { urns: verifiedUrns }, skip: !verifiedUrns.length });
|
||||||
urns.forEach((urn) => {
|
const entities: Entity[] = Array.isArray(data?.entities) ? (data?.entities.filter(Boolean) as Entity[]) : [];
|
||||||
if (urn.startsWith('urn:li:') && !verifiedUrns.includes(urn)) {
|
return { entities, loading };
|
||||||
setVerifiedUrns((prevUrns) => [...prevUrns, urn]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [urns, verifiedUrns]);
|
|
||||||
|
|
||||||
const { data } = useGetEntitiesQuery({ variables: { urns: verifiedUrns }, skip: !verifiedUrns.length });
|
|
||||||
return (data?.entities || []) as Entity[];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,9 @@ fragment PageModule on DataHubPageModule {
|
|||||||
richTextParams {
|
richTextParams {
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
assetCollectionParams {
|
||||||
|
assetUrns
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
namespace com.linkedin.module
|
||||||
|
|
||||||
|
import com.linkedin.common.Urn
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The params required if the module is type ASSET_COLLECTION
|
||||||
|
*/
|
||||||
|
record AssetCollectionModuleParams {
|
||||||
|
assetUrns: array[Urn]
|
||||||
|
}
|
||||||
@ -19,4 +19,8 @@ record DataHubPageModuleParams {
|
|||||||
richTextParams: optional record RichTextModuleParams {
|
richTextParams: optional record RichTextModuleParams {
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* The params required if the module is type ASSET_COLLECTION
|
||||||
|
*/
|
||||||
|
assetCollectionParams: optional AssetCollectionModuleParams
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user