Skip to content

Commit bcf74f3

Browse files
authored
Merge pull request #691 from contember/feat/dimensions
dimensions switcher and renderer
2 parents 7e4f3cd + 0053696 commit bcf74f3

File tree

26 files changed

+765
-20
lines changed

26 files changed

+765
-20
lines changed

build/api/admin.api.md

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3776,9 +3776,6 @@ identityId: GQLVariableType<string, true>;
37763776
memberships: GQLVariableType<Membership[], true>;
37773777
}>, TenantMutationResponse<never, UpdateMembershipErrorCodes>>;
37783778

3779-
// @public (undocumented)
3780-
export const Variable: React.MemoExoticComponent<({ name, format }: VariableProps) => ReactElement>;
3781-
37823779
// @public (undocumented)
37833780
export interface VariableConfig {
37843781
// (undocumented)
@@ -3788,14 +3785,6 @@ export interface VariableConfig {
37883785
}>;
37893786
}
37903787

3791-
// @public (undocumented)
3792-
export interface VariableProps {
3793-
// (undocumented)
3794-
format?: (value: ReactNode) => ReactNode;
3795-
// (undocumented)
3796-
name: Environment.Name;
3797-
}
3798-
37993788
// @public (undocumented)
38003789
export interface Variables {
38013790
// (undocumented)

build/api/react-binding.api.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import type { HasManyRelationMarker } from '@contember/binding';
2626
import type { HasOneRelationMarker } from '@contember/binding';
2727
import { JSX as JSX_2 } from 'react/jsx-runtime';
2828
import { MarkerTreeRoot } from '@contember/binding';
29+
import { MemoExoticComponent } from 'react';
2930
import { NamedExoticComponent } from 'react';
3031
import type { Persist } from '@contember/binding';
3132
import { PropsWithChildren } from 'react';
33+
import * as React_2 from 'react';
3234
import { ReactElement } from 'react';
3335
import { ReactNode } from 'react';
3436
import type { RelativeEntityList } from '@contember/binding';
@@ -169,6 +171,16 @@ export interface DeferredSubTreesProps {
169171
fallback: ReactNode;
170172
}
171173

174+
// @public (undocumented)
175+
export const DimensionRenderer: React_2.NamedExoticComponent<DimensionRendererProps>;
176+
177+
// @public (undocumented)
178+
export type DimensionRendererProps = {
179+
dimension: string;
180+
as: string;
181+
children: ReactNode;
182+
};
183+
172184
// @public (undocumented)
173185
export const DirtinessContext: Context<boolean>;
174186

@@ -766,6 +778,17 @@ export const useSortedEntities: (entityList: EntityListAccessor, sortableByField
766778
// @public (undocumented)
767779
export const useTreeRootId: () => TreeRootId | undefined;
768780

781+
// @public (undocumented)
782+
export const Variable: MemoExoticComponent<({ name, format }: VariableProps) => ReactElement>;
783+
784+
// @public (undocumented)
785+
export interface VariableProps {
786+
// (undocumented)
787+
format?: (value: ReactNode) => ReactNode;
788+
// (undocumented)
789+
name: Environment.Name;
790+
}
791+
769792

770793
export * from "@contember/binding";
771794

build/api/react-routing.api.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,32 @@ import { JSX as JSX_2 } from 'react/jsx-runtime';
1515
import { NamedExoticComponent } from 'react';
1616
import { ReactElement } from 'react';
1717
import { ReactNode } from 'react';
18+
import { StateStorageOrName } from '@contember/react-utils';
1819

1920
// @public (undocumented)
2021
export const createBindingLinkParametersResolver: (entity: EntityAccessor | undefined) => RoutingParameterResolver;
2122

2223
// @public (undocumented)
2324
export const CurrentRequestContext: Context<RequestState>;
2425

26+
// @public (undocumented)
27+
export const DimensionLink: NamedExoticComponent<DimensionLinkProps>;
28+
29+
// @public (undocumented)
30+
export type DimensionLinkAction = 'add' | 'toggle' | 'set' | 'unset';
31+
32+
// @public (undocumented)
33+
export interface DimensionLinkProps {
34+
// (undocumented)
35+
action?: DimensionLinkAction;
36+
// (undocumented)
37+
children: ReactElement;
38+
// (undocumented)
39+
dimension: string;
40+
// (undocumented)
41+
value: string;
42+
}
43+
2544
// @public (undocumented)
2645
export type DynamicRequestParameters = RequestParameters<RoutingParameter>;
2746

@@ -269,6 +288,13 @@ export const useBindingLinkParametersResolver: () => RoutingParameterResolver;
269288
// @public (undocumented)
270289
export const useCurrentRequest: () => RequestState;
271290

291+
// @public (undocumented)
292+
export const useDimensionState: ({ dimension, defaultValue, storage }: {
293+
dimension: string;
294+
defaultValue: string | string[];
295+
storage?: StateStorageOrName | undefined;
296+
}) => string[];
297+
272298
// @public (undocumented)
273299
export const useLinkFactory: () => (target: RoutingLinkTarget, parameters?: RequestParameters, entity?: EntityAccessor) => RoutingLinkParams;
274300

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export * from './SideDimensions'
22
export * from './DimensionsSwitcher'
3-
export * from './Variable'

packages/playground/admin/app/components/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as React from 'react'
12
import { memo, PropsWithChildren } from 'react'
23
import { IdentityLoader } from '../../lib/components/binding/identity'
34
import { Slots } from '../../lib/components/slots'

packages/playground/admin/app/components/navigation.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ArchiveIcon, BrushIcon, FormInputIcon, GripVertical, HomeIcon, KanbanIcon, TableIcon, UploadIcon } from 'lucide-react'
1+
import { ArchiveIcon, BrushIcon, FormInputIcon, GripVertical, HomeIcon, KanbanIcon, LanguagesIcon, TableIcon, UploadIcon } from 'lucide-react'
22
import { Menu, MenuItem, MenuList } from '../../lib/components/ui/menu'
33

44

@@ -35,6 +35,7 @@ export const Navigation = () => {
3535
<MenuItem icon={line} label={'Has many select'} to={'select/hasMany'} />
3636
<MenuItem icon={line} label={'Has many sortable select'} to={'select/hasManySortable'} />
3737
</MenuItem>
38+
<MenuItem icon={<LanguagesIcon size={16} />} label={'Dimensions'} to={'dimensions'} />
3839
</Menu>
3940
</div>
4041
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Slots } from '../../lib/components/slots'
2+
import { Binding, PersistButton } from '../../lib/components/binding'
3+
import * as React from 'react'
4+
import { DimensionsSwitcher, SideDimensions } from '../../lib/components/dimensions'
5+
import { EntitySubTree, Field, Variable } from '@contember/interface'
6+
import { InputField, TextareaField } from '../../lib/components/form'
7+
import { Card, CardContent, CardHeader, CardTitle } from '../../lib/components/ui/card'
8+
9+
export default () => {
10+
return <>
11+
<Binding>
12+
<DimensionsSwitcher
13+
options="DimensionsLocale"
14+
slugField="code"
15+
dimension="locale"
16+
isMulti
17+
>
18+
<Field field="label" />
19+
</DimensionsSwitcher>
20+
</Binding>
21+
22+
<Binding>
23+
<Slots.Actions><PersistButton /></Slots.Actions>
24+
<EntitySubTree entity="DimensionsItem(unique=unique)">
25+
<SideDimensions dimension="locale" as="currentLocale" field="locales(locale.code=$currentLocale)">
26+
<Card>
27+
<CardHeader>
28+
<CardTitle><Variable name="currentLocale" /></CardTitle>
29+
</CardHeader>
30+
<CardContent>
31+
<InputField field="title" />
32+
<TextareaField field="content" />
33+
</CardContent>
34+
</Card>
35+
</SideDimensions>
36+
</EntitySubTree>
37+
</Binding>
38+
</>
39+
}
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import * as React from 'react'
22

33
export default () => {
4-
debugger
5-
const foo = React.useMemo(() => 1, [])
64
return <>Hello!</>
75
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import {
2+
Component,
3+
DimensionLink,
4+
DimensionRenderer,
5+
Entity,
6+
EntityAccessor,
7+
Field,
8+
HasOne,
9+
StaticRender,
10+
SugaredQualifiedEntityList,
11+
SugaredRelativeSingleEntity,
12+
SugaredRelativeSingleField,
13+
useDimensionState,
14+
useEntity,
15+
} from '@contember/interface'
16+
import { DataView, DataViewEachRow, DataViewLoaderState, DataViewSortingDirections, useDataViewEntityListAccessor } from '@contember/react-dataview'
17+
import * as React from 'react'
18+
import { ReactNode, useMemo } from 'react'
19+
import { CheckIcon } from 'lucide-react'
20+
import { Loader } from './ui/loader'
21+
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'
22+
import { Button } from './ui/button'
23+
24+
export interface DimensionsSwitcherProps {
25+
options: SugaredQualifiedEntityList['entities']
26+
orderBy?: DataViewSortingDirections
27+
dimension: string
28+
children: ReactNode
29+
slugField: SugaredRelativeSingleField['field']
30+
isMulti?: boolean
31+
}
32+
33+
export const DimensionsSwitcher = Component(({ options, dimension, children, slugField, orderBy, isMulti }: DimensionsSwitcherProps) => {
34+
return (
35+
<DataView entities={options} initialSorting={orderBy}>
36+
<DataViewLoaderState initial refreshing>
37+
<Loader position={'static'} />
38+
</DataViewLoaderState>
39+
<DataViewLoaderState loaded>
40+
41+
<Popover>
42+
<PopoverTrigger>
43+
<Button variant={'outline'} size="sm">
44+
<DimensionSwitcherCurrentValues dimension={dimension} slugField={slugField}>
45+
{children}
46+
</DimensionSwitcherCurrentValues>
47+
</Button>
48+
</PopoverTrigger>
49+
<PopoverContent className="p-2" align="start">
50+
<div className="flex flex-col gap-1">
51+
<DataViewEachRow>
52+
<DimensionSwitcherItem dimension={dimension} slugField={slugField} isMulti={isMulti}>
53+
{children}
54+
<StaticRender>
55+
<Field field={slugField} />
56+
</StaticRender>
57+
</DimensionSwitcherItem>
58+
</DataViewEachRow>
59+
</div>
60+
</PopoverContent>
61+
</Popover>
62+
</DataViewLoaderState>
63+
</DataView>
64+
)
65+
})
66+
67+
const DimensionSwitcherCurrentValues = ({ children, dimension, slugField }: { children: ReactNode, dimension: string, slugField: SugaredRelativeSingleField['field'] }) => {
68+
const entitiesBySlug = useDimensionEntitiesBySlug(slugField)
69+
70+
const currentDimensionValue = useDimensionState({
71+
dimension,
72+
defaultValue: Object.keys(entitiesBySlug)[0],
73+
storage: 'local',
74+
})
75+
76+
const values = useMemo(() => currentDimensionValue.map(it => entitiesBySlug[it]).filter(Boolean), [currentDimensionValue, entitiesBySlug])
77+
78+
return (
79+
<div className="flex gap-1">
80+
{values.map(it => (
81+
<Entity key={it.key} accessor={it}>
82+
<div className={'gap-1 group text-black text-left inline-flex items-center px-1 text-sm border-b'}>
83+
<span>{children}</span>
84+
</div>
85+
</Entity>
86+
))}
87+
</div>
88+
)
89+
}
90+
91+
92+
const DimensionSwitcherItem = ({ children, dimension, slugField, isMulti }: { children: ReactNode, dimension: string, slugField: SugaredRelativeSingleField['field'], isMulti?: boolean }) => {
93+
const entity = useEntity()
94+
const slugValue = entity.getField<string>(slugField).value
95+
if (!slugValue) {
96+
return null
97+
}
98+
99+
return (
100+
<DimensionLink dimension={dimension} value={slugValue} action={isMulti ? 'toggle' : 'set'}>
101+
<a className={'gap-1 group text-gray-800 text-left inline-flex items-center px-1 py-1 text-sm rounded transition-all hover:bg-accent hover:text-accent-foreground group data-[active]:text-black'}>
102+
<CheckIcon className={'w-3 h-3 hidden group-data-[active]:block'} />
103+
<span className={'w-3 h-3 group-data-[active]:hidden'} />
104+
<span>{children}</span>
105+
</a>
106+
</DimensionLink>
107+
)
108+
}
109+
110+
export interface SideDimensionsProps {
111+
dimension: string
112+
as: string
113+
field: SugaredRelativeSingleEntity['field']
114+
children: ReactNode
115+
}
116+
117+
export const SideDimensions = Component<SideDimensionsProps>(({ dimension, children, as, field }) => {
118+
return (
119+
<div className="flex mt-4 gap-4">
120+
<DimensionRenderer dimension={dimension} as={as}>
121+
<HasOne field={field}>
122+
<div className="flex-1">
123+
{children}
124+
</div>
125+
</HasOne>
126+
</DimensionRenderer>
127+
</div>
128+
)
129+
})
130+
131+
132+
const useDimensionEntitiesBySlug = (slugField: SugaredRelativeSingleField['field']): Record<string, EntityAccessor> => {
133+
const accessor = useDataViewEntityListAccessor()
134+
return useMemo(() => Object.fromEntries(Array.from(accessor ?? []).map(it => [it.getField<string>(slugField).value, it])), [accessor, slugField])
135+
}

0 commit comments

Comments
 (0)