Skip to content
Open
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,5 @@ schema.graphql
/.husky

api/.claude/

CLAUDE.md
1 change: 1 addition & 0 deletions ui/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export { BlockImageTitleTextButtonWrapper } from '~content-blocks/BlockImageTitl
export { BlockIntro } from '~content-blocks/BlockIntro';
export { BlockKlaar } from '~content-blocks/BlockKlaar';
export { BlockMaintainersGrid } from '~content-blocks/BlockMaintainersGrid';
export { BlockObjectsGrid } from '~content-blocks/BlockObjectsGrid';
export { BlockOverviewNewspaperTitles } from '~content-blocks/BlockOverviewNewspaperTitles';
export { BlockOverviewWithCarousel } from '~content-blocks/BlockOverviewWithCarousel';
export { BlockPageOverviewWrapper } from '~content-blocks/BlockPageOverview/BlockPageOverview.wrapper';
Expand Down
26 changes: 24 additions & 2 deletions ui/src/react-admin/core/config/config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import type {
AvoUserCommonUser,
} from '@viaa/avo2-types';
import type { ComponentType, FC, FunctionComponent, MouseEvent, ReactNode } from 'react';

import type { OrderProperty } from '~content-blocks/BlockObjectsGrid/BlockObjectsGrid.types.ts';
import type { ContentBlockType } from '~modules/content-page/types/content-block.types';
import type {
ContentPageInfo,
ContentPageWidth,
} from '~modules/content-page/types/content-pages.types';
import type { App, Locale } from '~modules/translations/translations.core.types';

import type { UserBulkAction } from '~modules/user/user.types';
import type { FlowPlayerWrapperProps } from '~shared/components/FlowPlayerWrapper/FlowPlayerWrapper.types';

Expand Down Expand Up @@ -56,6 +55,22 @@ export interface LinkInfo {

export type NavigateFunction = (to: string, options?: { replace?: boolean }) => Promise<void>;

export interface SearchFilter {
field: string;
operator: string;
value?: string;
multiValue?: string[];
}

export interface IeObjectsSearchBody {
filters: SearchFilter[];
size: number;
page: number;
requestedAggs?: string[];
orderProp?: OrderProperty;
orderDirection?: 'asc' | 'desc';
}

export interface AdminConfig {
// Core module configurations
flowplayer: {
Expand Down Expand Up @@ -88,6 +103,13 @@ export interface AdminConfig {
queryCache: {
clear: (key: string) => Promise<void>;
};
search?: {
// Converts a hetarchief search-page url (as stored on eg. the ObjectsGrid content-page
// block) into an ie-objects search API request body. Lives in the config so the client's
// own url-filter-mapping logic (used by its search page) can be reused here, without the
// admin-core needing to depend on the client package.
clientSearchUrlToApiSearchUrl: (searchQuery: string) => IeObjectsSearchBody;
};
};
components: {
loader: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { BlockImageTitleTextButtonWrapper } from '~content-blocks/BlockImageTitl
import { BlockIntro } from '~content-blocks/BlockIntro';
import { BlockKlaar } from '~content-blocks/BlockKlaar';
import { BlockMaintainersGrid } from '~content-blocks/BlockMaintainersGrid';
import { BlockObjectsGrid } from '~content-blocks/BlockObjectsGrid';
import { BlockOverviewNewspaperTitles } from '~content-blocks/BlockOverviewNewspaperTitles';
import { BlockOverviewWithCarousel } from '~content-blocks/BlockOverviewWithCarousel/BlockOverviewWithCarousel.tsx';
import { BlockPageOverviewWrapper } from '~content-blocks/BlockPageOverview';
Expand Down Expand Up @@ -87,6 +88,7 @@ export function GET_BLOCK_COMPONENT(
[ContentBlockType.Breadcrumbs]: BlockBreadcrumbs,
[ContentBlockType.HetArchiefImageTextBackground]: BlockHetArchiefImageTextBackground,
[ContentBlockType.OverviewWithCarousel]: BlockOverviewWithCarousel,
[ContentBlockType.ObjectsGrid]: BlockObjectsGrid,

// Avo specific blocks
[ContentBlockType.MediaGrid]: loadComponentFromConfig(ContentBlockType.MediaGrid),
Expand All @@ -104,7 +106,7 @@ export function GET_BLOCK_COMPONENT(
*/
export const REPEATABLE_CONTENT_BLOCKS = [
ContentBlockType.AnchorLinks,
ContentBlockType.Buttons,
// ContentBlockType.Buttons, // migrated to mechanism B (fieldGroup `elements`) - ARC-3779
ContentBlockType.CTAs,
ContentBlockType.ImageGrid,
ContentBlockType.MediaGrid,
Expand Down Expand Up @@ -142,6 +144,7 @@ export const NAVIGABLE_CONTENT_BLOCKS = [
ContentBlockType.OverviewNewspaperTitles,
ContentBlockType.ContentEncloseGrid,
ContentBlockType.Breadcrumbs,
ContentBlockType.ObjectsGrid,
];

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { ContentBlockEditor, ContentBlockType } from '../../../types/content-blo

import { ALIGN_FIELD, BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, TEXT_FIELD } from '../defaults';

export const INITIAL_BUTTONS_COMPONENTS_STATE = (): ButtonsBlockComponentState[] => [
{
label: '',
type: 'primary',
},
];
const INITIAL_BUTTON_ELEMENT_STATE = (): ButtonsBlockComponentState => ({
label: '',
type: 'primary',
});

export const INITIAL_BUTTONS_COMPONENTS_STATE = (): { elements: ButtonsBlockComponentState[] } => ({
elements: [INITIAL_BUTTON_ELEMENT_STATE()],
});

export const INITIAL_BUTTONS_BLOCK_STATE = (): DefaultContentBlockState =>
BLOCK_STATE_DEFAULTS({
Expand All @@ -31,40 +33,55 @@ export const BUTTONS_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({
name: tText('admin/content-block/helpers/generators/buttons___knoppen'),
type: ContentBlockType.Buttons,
components: {
name: tText('admin/content-block/helpers/generators/buttons___knop'),
limits: {
max: 3,
},
state: INITIAL_BUTTONS_COMPONENTS_STATE(),
fields: {
type: {
label: tText('admin/content-block/helpers/generators/buttons___type'),
editorType: ContentBlockEditor.Select,
editorProps: {
options: AdminConfigManager.getConfig().components.buttonTypes(),
},
},
label: TEXT_FIELD(
{
label: tText('admin/content-block/helpers/generators/buttons___tekst'),
// Mechanism B: the buttons repeat as a fieldGroup under `elements`.
// See components/blocks/README.md.
elements: {
label: tText('admin/content-block/helpers/generators/buttons___knop'),
type: 'fieldGroup',
max: 3,
repeat: {
defaultState: INITIAL_BUTTON_ELEMENT_STATE(),
addButtonLabel: tText(
'admin/content-block/components/content-block-form/content-block-form___voeg-label-to',
{ label: tText('admin/content-block/helpers/generators/buttons___knop') }
),
deleteButtonLabel: tText(
'admin/content-block/components/content-block-form/content-block-form___verwijder-sectie'
),
},
tText('admin/content-block/helpers/generators/buttons___knoptekst-is-verplicht')
),
altTitle: TEXT_FIELD({
label: tText('admin/content-block/helpers/generators/buttons___alt-title-text'),
validator: undefined,
}),
icon: {
label: tText('admin/content-block/helpers/generators/buttons___icoon'),
editorType: ContentBlockEditor.IconPicker,
editorProps: {
options: GET_ADMIN_ICON_OPTIONS(),
fields: {
type: {
label: tText('admin/content-block/helpers/generators/buttons___type'),
editorType: ContentBlockEditor.Select,
editorProps: {
options: AdminConfigManager.getConfig().components.buttonTypes(),
},
},
label: TEXT_FIELD(
{
label: tText('admin/content-block/helpers/generators/buttons___tekst'),
},
tText('admin/content-block/helpers/generators/buttons___knoptekst-is-verplicht')
),
altTitle: TEXT_FIELD({
label: tText('admin/content-block/helpers/generators/buttons___alt-title-text'),
validator: undefined,
}),
icon: {
label: tText('admin/content-block/helpers/generators/buttons___icoon'),
editorType: ContentBlockEditor.IconPicker,
editorProps: {
options: GET_ADMIN_ICON_OPTIONS(),
},
},
buttonAction: {
label: tText('admin/content-block/helpers/generators/buttons___knop-actie'),
editorType: ContentBlockEditor.ContentPicker,
},
},
},
buttonAction: {
label: tText('admin/content-block/helpers/generators/buttons___knop-actie'),
editorType: ContentBlockEditor.ContentPicker,
},
},
},
block: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { AvoCoreContentPickerType } from '@viaa/avo2-types';
import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, TEXT_FIELD } from '~content-blocks/defaults';
import { GET_HEADING_TYPE_OPTIONS } from '~modules/content-page/const/get-heading-type-options';
import {
type ContentBlockConfig,
ContentBlockEditor,
type ContentBlockField,
ContentBlockType,
type DefaultContentBlockState,
type HeadingTypeOption,
} from '~modules/content-page/types/content-block.types';
import type { PickerItem } from '~modules/shared/types/content-picker';
import { tHtml, tText } from '~shared/helpers/translation-functions';
import { HET_ARCHIEF } from '~shared/types';

// The grid can hold at most 3 "fixed positions" that are always shown (see FA).
const MAX_FIXED_POSITIONS = 3;

export interface ObjectsGridFixedPositionState {
// A single ie-object (referenced by its pid / fragmentId) that is pinned to a fixed position.
mediaItem: PickerItem;
}

export interface ObjectsGridBlockComponentState {
title: string;
titleType: HeadingTypeOption;
// A hetarchief.be search url, e.g. https://hetarchief.be/zoeken?aanbieders=OR-...&page=1
searchQuery: string;
// Optional (0 to 3) objects that are always shown, in the order they are entered.
elements: ObjectsGridFixedPositionState[];
}

const INITIAL_OBJECTS_GRID_FIXED_POSITION_STATE = (): ObjectsGridFixedPositionState => ({
mediaItem: {
label: '',
type: AvoCoreContentPickerType.IE_OBJECT,
value: '',
},
});

export const INITIAL_OBJECTS_GRID_COMPONENTS_STATE = (): ObjectsGridBlockComponentState => ({
title: '',
titleType: 'h2',
searchQuery: '',
elements: [],
});

export const INITIAL_OBJECTS_GRID_BLOCK_STATE = (): DefaultContentBlockState => ({
...BLOCK_STATE_DEFAULTS({
padding: {
top: 'top-large',
bottom: 'bottom-large',
},
}),
});

export const OBJECTS_GRID_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({
position,
name: tText(
'modules/content-page/components/blocks/block-objects-grid/block-objects-grid___objecten-grid',
undefined,
[HET_ARCHIEF]
),
type: ContentBlockType.ObjectsGrid,
components: {
state: INITIAL_OBJECTS_GRID_COMPONENTS_STATE(),
fields: {
// Optional title, shown at the top of the block.
title: TEXT_FIELD({
label: tText(
'modules/content-page/components/blocks/block-objects-grid/block-objects-grid___titel',
undefined,
[HET_ARCHIEF]
),
validator: undefined,
}),
titleType: {
label: tText(
'modules/content-page/components/blocks/block-objects-grid/block-objects-grid___titel-grootte',
undefined,
[HET_ARCHIEF]
),
editorType: ContentBlockEditor.Select,
editorProps: {
options: GET_HEADING_TYPE_OPTIONS(),
},
},
// Required search url on hetarchief.be that fills the grid.
searchQuery: TEXT_FIELD({
label: tText(
'modules/content-page/components/blocks/block-objects-grid/block-objects-grid___zoekfilter-url',
undefined,
[HET_ARCHIEF]
),
note: tHtml(
'modules/content-page/components/blocks/block-objects-grid/block-objects-grid___geef-een-zoek-url-van-hetarchief-be-in-bijvoorbeeld-https-hetarchief-be-zoeken-aanbieders-or-page-1',
undefined,
[HET_ARCHIEF]
),
validator: (value: string) => {
const errors: string[] = [];

if (!value) {
errors.push(
tText(
'modules/content-page/components/blocks/block-objects-grid/block-objects-grid___een-zoekfilter-is-verplicht',
undefined,
[HET_ARCHIEF]
)
);
return errors;
}

try {
new URL(value);
} catch {
errors.push(
tText(
'modules/content-page/components/blocks/block-objects-grid/block-objects-grid___gelieve-een-geldige-url-in-te-vullen',
undefined,
[HET_ARCHIEF]
)
);
}

return errors;
},
}),
// Up to 3 optional fixed positions, each pinned to a specific ie-object.
elements: {
label: tText(
'modules/content-page/components/blocks/block-objects-grid/block-objects-grid___vaste-positie',
undefined,
[HET_ARCHIEF]
),
type: 'fieldGroup',
min: 0,
max: MAX_FIXED_POSITIONS,
fields: {
mediaItem: {
label: tText(
'modules/content-page/components/blocks/block-objects-grid/block-objects-grid___object-pid-of-fragment-id',
undefined,
[HET_ARCHIEF]
),
editorType: ContentBlockEditor.ContentPicker,
editorProps: {
allowedTypes: [AvoCoreContentPickerType.IE_OBJECT],
hideTypeDropdown: true,
},
} as ContentBlockField,
},
repeat: {
defaultState: INITIAL_OBJECTS_GRID_FIXED_POSITION_STATE(),
addButtonLabel: tText(
'modules/content-page/components/blocks/block-objects-grid/block-objects-grid___voeg-een-vaste-positie-toe',
undefined,
[HET_ARCHIEF]
),
deleteButtonLabel: tText(
'modules/content-page/components/blocks/block-objects-grid/block-objects-grid___verwijder-vaste-positie',
undefined,
[HET_ARCHIEF]
),
},
},
},
},
block: {
state: INITIAL_OBJECTS_GRID_BLOCK_STATE(),
fields: {
// Includes the block-wide background color (see FA), padding, margin, user groups and anchor.
...BLOCK_FIELD_DEFAULTS(),
},
},
});
Loading