diff --git a/apps/admin-server/package.json b/apps/admin-server/package.json index 74d472255..7fcbc348d 100644 --- a/apps/admin-server/package.json +++ b/apps/admin-server/package.json @@ -31,6 +31,7 @@ "http-proxy-middleware": "^2.0.6", "leaflet": "^1.9.4", "leaflet-draw": "^1.0.4", + "lodash": "^4.17.21", "lucide-react": "^0.276.0", "next": "^13.4.17", "next-auth": "^4.23.1", @@ -57,6 +58,7 @@ "@types/cron": "^2.4.0", "@types/leaflet": "^1.9.6", "@types/leaflet-draw": "^1.0.9", + "@types/lodash": "^4.14.202", "@types/node": "20.5.0", "@types/react": "18.2.20", "@types/react-csv": "^1.1.8", diff --git a/apps/admin-server/src/lib/EditFieldProps.ts b/apps/admin-server/src/lib/form-widget-helpers/EditFieldProps.ts similarity index 100% rename from apps/admin-server/src/lib/EditFieldProps.ts rename to apps/admin-server/src/lib/form-widget-helpers/EditFieldProps.ts diff --git a/apps/admin-server/src/lib/form-widget-helpers/index.tsx b/apps/admin-server/src/lib/form-widget-helpers/index.tsx new file mode 100644 index 000000000..86320193c --- /dev/null +++ b/apps/admin-server/src/lib/form-widget-helpers/index.tsx @@ -0,0 +1,42 @@ +import { ControllerRenderProps } from 'react-hook-form'; +import { + FormControl, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Input } from '@openstad/ui/src'; + +// Simple yes/no selector that uses a props.onFieldchanged method to emit changes +export function YesNoSelect( + field: ControllerRenderProps, + props: { onFieldChanged?: (key: string, value: any) => void } +) { + return ( + + ); +} diff --git a/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/form.tsx b/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/form.tsx index 939ccb91f..5a761ec1b 100644 --- a/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/form.tsx +++ b/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/form.tsx @@ -13,7 +13,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; import { CommentsWidgetProps } from '@openstad/comments/src/comments'; -import { EditFieldProps } from '@/lib/EditFieldProps'; +import { EditFieldProps } from '@/lib/form-widget-helpers/EditFieldProps'; import { useFieldDebounce } from '@/hooks/useFieldDebounce'; const formSchema = z.object({ diff --git a/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/general.tsx b/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/general.tsx index af55aaa22..230208ea7 100644 --- a/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/general.tsx +++ b/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/general.tsx @@ -19,7 +19,7 @@ import { Heading } from '@/components/ui/typography'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; -import { EditFieldProps } from '@/lib/EditFieldProps'; +import { EditFieldProps } from '@/lib/form-widget-helpers/EditFieldProps'; import { CommentsWidgetProps } from '@openstad/comments/src/comments'; const formSchema = z.object({ diff --git a/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/list.tsx b/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/list.tsx index bbe4b79e4..1127f1c62 100644 --- a/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/list.tsx +++ b/apps/admin-server/src/pages/projects/[project]/widgets/comments/[id]/list.tsx @@ -10,7 +10,7 @@ import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { Heading } from '@/components/ui/typography'; import { useFieldDebounce } from '@/hooks/useFieldDebounce'; -import { EditFieldProps } from '@/lib/EditFieldProps'; +import { EditFieldProps } from '@/lib/form-widget-helpers/EditFieldProps'; import { zodResolver } from '@hookform/resolvers/zod'; import { CommentsWidgetProps } from '@openstad/comments/src/comments'; import { useForm } from 'react-hook-form'; diff --git a/apps/admin-server/src/pages/projects/[project]/widgets/likes/[id]/weergave.tsx b/apps/admin-server/src/pages/projects/[project]/widgets/likes/[id]/weergave.tsx index 831a680c6..5518c4885 100644 --- a/apps/admin-server/src/pages/projects/[project]/widgets/likes/[id]/weergave.tsx +++ b/apps/admin-server/src/pages/projects/[project]/widgets/likes/[id]/weergave.tsx @@ -22,7 +22,7 @@ import * as z from 'zod'; import { Heading } from '@/components/ui/typography'; import { Separator } from '@/components/ui/separator'; import { LikeProps } from '@openstad/likes/src/likes'; -import { EditFieldProps } from '@/lib/EditFieldProps'; +import { EditFieldProps } from '@/lib/form-widget-helpers/EditFieldProps'; import { useFieldDebounce } from '@/hooks/useFieldDebounce'; const formSchema = z.object({ diff --git a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/display.tsx b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/display.tsx index ae88de1e5..2eb6e6875 100644 --- a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/display.tsx +++ b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/display.tsx @@ -7,75 +7,65 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; import { Heading } from '@/components/ui/typography'; -import { useWidgetConfig } from '@/hooks/use-widget-config'; +import { YesNoSelect } from '@/lib/form-widget-helpers'; +import { EditFieldProps } from '@/lib/form-widget-helpers/EditFieldProps'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useEffect } from 'react'; +import { ResourceOverviewWidgetProps } from '@openstad/resource-overview/src/resource-overview'; +import { Input } from '@/components/ui/input'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; +import { useFieldDebounce } from '@/hooks/useFieldDebounce'; const formSchema = z.object({ displayTitle: z.boolean(), - displayRanking: z.boolean(), - displayLabel: z.boolean(), - displaySummary: z.boolean(), + titleMaxLength: z.coerce.number(), displayDescription: z.boolean(), + descriptionMaxLength: z.coerce.number(), + displaySummary: z.boolean(), + summaryMaxLength: z.coerce.number(), + displayArguments: z.boolean(), displayVote: z.boolean(), - displayShareButtons: z.boolean(), - displayEditLink: z.boolean(), - displayCaption: z.boolean(), + // displayRanking: z.boolean(), + // displayLabel: z.boolean(), + // displayShareButtons: z.boolean(), + // displayEditLink: z.boolean(), + // displayCaption: z.boolean(), }); -export default function WidgetResourceOverviewDisplay() { +export default function WidgetResourceOverviewDisplay( + props: ResourceOverviewWidgetProps & + EditFieldProps +) { type FormData = z.infer; - const category = 'display'; - - const { - data: widget, - isLoading: isLoadingWidget, - updateConfig, - } = useWidgetConfig(); - - const defaults = () => ({ - displayTitle: widget?.config?.[category]?.displayTitle || false, - displayRanking: widget?.config?.[category]?.displayRanking || false, - displayLabel: widget?.config?.[category]?.displayLabel || false, - displaySummary: widget?.config?.[category]?.displaySummary || false, - displayDescription: widget?.config?.[category]?.displayDescription || false, - displayArguments: widget?.config?.[category]?.displayArguments || false, - displayVote: widget?.config?.[category]?.displayVote || false, - displayShareButtons: - widget?.config?.[category]?.displayShareButtons || false, - displayEditLink: widget?.config?.[category]?.displayEditLink || false, - displayCaption: widget?.config?.[category]?.displayCaption || false, - }); async function onSubmit(values: FormData) { - try { - await updateConfig({ [category]: values }); - } catch (error) { - console.error('could falset update', error); - } + props.updateConfig({ ...props, ...values }); } + const { onFieldChange } = useFieldDebounce(props.onFieldChanged); + const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: defaults(), + defaultValues: { + displayTitle: props?.displayTitle || false, + titleMaxLength: props?.titleMaxLength || 20, + displayDescription: props?.displayDescription || false, + descriptionMaxLength: props?.descriptionMaxLength || 20, + displaySummary: props?.displaySummary || false, + summaryMaxLength: props?.summaryMaxLength || 30, + displayArguments: props?.displayArguments || false, + displayVote: props?.displayVote || false, + // displayRanking: props?.displayRanking || false, + // displayLabel: props?.displayLabel || false, + // displayShareButtons: props?.displayShareButtons || false, + // displayEditLink: props?.displayEditLink || false, + // displayCaption: props?.displayCaption || false, + }, }); - useEffect(() => { - form.reset(defaults()); - }, [widget]); - return (
@@ -90,134 +80,145 @@ export default function WidgetResourceOverviewDisplay() { render={({ field }) => ( Titel weergeven - + {YesNoSelect(field, props)} )} /> + ( + + + Hoeveelheid karakters van de titel die getoond wordt + + + { + onFieldChange(field.name, e.target.value); + field.onChange(e); + }} + /> + + + + )} + /> + {/* ( Ranking weergeven - + {YesNoSelect(field, props)} )} - /> - */} + {/* ( Label weergeven - + {YesNoSelect(field, props)} + + + )} + /> */} + {/* ( + + Samenvatting weergeven + {YesNoSelect(field, props)} + + + )} + /> */} + + ( + + Beschrijving weergeven + {YesNoSelect(field, props)} + + + )} + /> + + ( + + + Hoeveelheid karakters van de beschrijving die getoond wordt + + + { + onFieldChange(field.name, e.target.value); + field.onChange(e); + }} + /> + )} /> + ( Samenvatting weergeven - + {YesNoSelect(field, props)} )} /> + ( - Beschrijving weergeven - + + Hoeveelheid karakters van de samenvatting die getoond wordt + + + { + onFieldChange(field.name, e.target.value); + field.onChange(e); + }} + /> + )} /> + ( Hoeveelheid aan argumenten weergeven - + {YesNoSelect(field, props)} )} @@ -230,69 +231,33 @@ export default function WidgetResourceOverviewDisplay() { Hoeveelheid stemmen weergeven (voor Gridder) - + {YesNoSelect(field, props)} )} /> - ( Deel knoppen weergeven - + {YesNoSelect(field, props)} )} - /> - */} + {/* ( Aanpas-link weergeven voor moderators - + {YesNoSelect(field, props)} )} - /> + /> */} diff --git a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/general.tsx b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/general.tsx index 23f17930a..fc96b61d4 100644 --- a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/general.tsx +++ b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/general.tsx @@ -30,7 +30,6 @@ const formSchema = z.object({ 'resourceUser', 'submission', ]), - enableVoting: z.boolean(), displayType: z.enum(['cardrow', 'cardgrid', 'raw']), }); @@ -46,7 +45,6 @@ export default function WidgetResourceOverviewGeneral() { const defaults = () => ({ resource: widget?.config?.[category]?.resource || 'resource', - enableVoting: widget?.config?.[category]?.enableVoting || false, displayType: widget?.config?.[category]?.displayType || 'cardrow', }); @@ -103,29 +101,6 @@ export default function WidgetResourceOverviewGeneral() { )} /> - ( - - Toestaan van stemmen - - - - )} - /> ; - const category = 'image'; - - const { - data: widget, - isLoading: isLoadingWidget, - updateConfig, - } = useWidgetConfig(); - - const defaults = () => ({ - defaultImage: widget?.config?.[category]?.defaultImage || '', - aspectRatio: widget?.config?.[category]?.aspectRatio || '1:1', - }); - - async function onSubmit(values: FormData) { - try { - await updateConfig({ [category]: values }); - } catch (error) { - console.error('could falset update', error); - } - } - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: defaults(), - }); - - useEffect(() => { - form.reset(defaults()); - }, [widget]); - - return ( -
- - Afbeeldingen - - - ( - - Default afbeelding - - - - - - )} - /> - ( - - - Aspect ratio van afbeeldingen binnen tiles - - - - - - - )} - /> - - - -
- ); -} diff --git a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/index.tsx b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/index.tsx index fef866b1f..7801011ee 100644 --- a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/index.tsx +++ b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/index.tsx @@ -7,25 +7,53 @@ import { TabsTrigger, } from '../../../../../../components/ui/tabs'; import WidgetResourceOverviewGeneral from './general'; -import WidgetResourceOverviewImage from './image'; import WidgetResourceOverviewDisplay from './display'; import WidgetResourceOverviewButton from './button'; import WidgetResourceOverviewSorting from './sorting'; import WidgetResourceOverviewPagination from './pagination'; -import WidgetResourceOverviewFilter from './filter'; import WidgetResourceOverviewSearch from './search'; import WidgetResourceOverviewTags from './tags'; import WidgetResourceOverviewInclude from './include'; -import WidgetResourceOverviewLabel from './label'; import WidgetResourceOverviewInfo from './info'; import { useRouter } from 'next/router'; -import Preview from '@/components/widget-preview'; +import { useWidgetConfig } from '@/hooks/use-widget-config'; +import { useWidgetPreview } from '@/hooks/useWidgetPreview'; +import { ResourceOverviewWidgetProps } from '@openstad/resource-overview/src/resource-overview'; +import WidgetPreview from '@/components/widget-preview'; export default function WidgetResourceOverview() { const router = useRouter(); const id = router.query.id; const projectId = router.query.project; + const { data: widget, updateConfig } = useWidgetConfig(); + const { previewConfig, updatePreview } = + useWidgetPreview({ + projectId, + resourceId: '2', + api: { + url: '/api/openstad', + }, + title: 'Vind je dit een goed idee?', + variant: 'medium', + }); + + const totalPropPackage = { + ...widget?.config, + updateConfig: (config: ResourceOverviewWidgetProps) => + updateConfig({ ...widget.config, ...config }), + + onFieldChanged: (key: keyof ResourceOverviewWidgetProps, value: any) => { + if (previewConfig) { + updatePreview({ + ...previewConfig, + [key]: value, + }); + } + }, + projectId, + }; + return (
- + - Preview Algemeen - Afbeeldingen + {/* Afbeeldingen */} Display - Knop teksten + {/* Knop teksten */} + Tags + Zoeken Sorteren Pagination - Filters - Zoeken - Tags Inclusief/exclusief - Labels Info - - {/* */} - - - - - + - + - + - + - - - - + - + - + - - - - + - + + +
+ {previewConfig ? ( + + ) : null} +
diff --git a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/info.tsx b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/info.tsx index 1dbb0a97e..7878cc741 100644 --- a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/info.tsx +++ b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/info.tsx @@ -25,7 +25,6 @@ import { useForm } from 'react-hook-form'; import * as z from 'zod'; const formSchema = z.object({ - summaryCharLength: z.coerce.number(), step1: z.string(), step2: z.string(), step2ButtonFeedback: z.string(), @@ -59,8 +58,6 @@ export default function WidgetResourceOverviewInfo() { excludeTheme: widget?.config?.[category]?.excludeTheme || '', filterTheme: widget?.config?.[category]?.filterTheme || '', filterResource: widget?.config?.[category]?.filterResource || '', - - summaryCharLength: widget?.config?.[category]?.summaryCharLength || 30, step1: widget?.config?.[category]?.step1 || "Kies uit onderstaand overzicht jouw favoriete ontwerp voor de muurtekst 'Zorg goed voor onze stad en voor elkaar', en vul in de volgende stap je gegevens in.", @@ -120,21 +117,6 @@ export default function WidgetResourceOverviewInfo() {
- ( - - - Hoeveelheid karakters waar de samenvatting uit mag bestaan - - - - - - - )} - /> +) { type FormData = z.infer; - const category = 'search'; - - const { - data: widget, - isLoading: isLoadingWidget, - updateConfig, - } = useWidgetConfig(); - - const defaults = () => ({ - displaySearch: widget?.config?.[category]?.displaySearch || false, - textActiveSearch: - widget?.config?.[category]?.textActiveSearch || - 'Je ziet hier zoekresultaten voor [zoekterm]', - }); async function onSubmit(values: FormData) { - try { - await updateConfig({ [category]: values }); - } catch (error) { - console.error('could falset update', error); - } + props.updateConfig({ ...props, ...values }); } const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: defaults(), + defaultValues: { + displaySearch: props.displaySearch || false, + textActiveSearch: + props.textActiveSearch || 'Je ziet hier zoekresultaten voor [zoekterm]', + }, }); - useEffect(() => { - form.reset(defaults()); - }, [widget]); + const { onFieldChange } = useFieldDebounce(props.onFieldChanged); return (
@@ -75,20 +66,8 @@ export default function WidgetResourceOverviewSearch() { name="displaySearch" render={({ field }) => ( - Toestaan van stemmen - + Zoekbalk weergeven + {YesNoSelect(field, props)} )} @@ -100,7 +79,13 @@ export default function WidgetResourceOverviewSearch() { Tekst voor actieve resultaten - + { + onFieldChange(field.name, e.target.value); + field.onChange(e); + }} + /> diff --git a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/sorting.tsx b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/sorting.tsx index 2fbda6b18..ecc33bb95 100644 --- a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/sorting.tsx +++ b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/sorting.tsx @@ -17,92 +17,51 @@ import { } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; import { Heading } from '@/components/ui/typography'; -import { useWidgetConfig } from '@/hooks/use-widget-config'; +import { YesNoSelect } from '@/lib/form-widget-helpers'; +import { EditFieldProps } from '@/lib/form-widget-helpers/EditFieldProps'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useEffect } from 'react'; +import { ResourceOverviewWidgetProps } from '@openstad/resource-overview/src/resource-overview'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; -const sorting = [ +// Defines the types allowed to go to the frontend +const SortingTypes = [ { - id: 'newest', + value: 'createdAt_desc', label: 'Nieuwste eerst', }, - { - id: 'oldest', - label: 'Oudste eerst', - }, - { - id: 'random', - label: 'Willekeurig', - }, - { - id: 'mostLikes', - label: 'Meeste likes', - }, - { - id: 'leastLikes', - label: 'Minste likes', - }, - { - id: 'highestCost', - label: 'Meeste reacties', - }, - { - id: 'lowestCost', - label: 'Minste reacties', - }, + { value: 'createdAt_asc', label: 'Oudste eerst' }, ]; const formSchema = z.object({ displaySorting: z.boolean(), - defaultSorting: z.enum([ - 'newest', - 'oldest', - 'random', - 'mostLikes', - 'leastLikes', - 'highestCost', - 'lowestCost', - ]), - sorting: z.array(z.string()).refine((value) => value.some((item) => item), { - message: 'You have to select at least one item.', - }), + defaultSorting: z.string(), + sorting: z + .array(z.object({ value: z.string(), label: z.string() })) + .refine((value) => value.some((item) => item), { + message: 'You have to select at least one item.', + }), }); -export default function WidgetResourceOverviewSorting() { +export default function WidgetResourceOverviewSorting( + props: ResourceOverviewWidgetProps & + EditFieldProps +) { type FormData = z.infer; - const category = 'sorting'; - - const { - data: widget, - isLoading: isLoadingWidget, - updateConfig, - } = useWidgetConfig(); - - const defaults = () => ({ - displaySorting: widget?.config?.[category]?.displaySorting || false, - defaultSorting: widget?.config?.[category]?.defaultSorting || 'newest', - sorting: widget?.config?.[category]?.sorting || [], - }); async function onSubmit(values: FormData) { - try { - await updateConfig({ [category]: values }); - } catch (error) { - console.error('could falset update', error); - } + props.updateConfig({ ...props, ...values }); } const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: defaults(), + defaultValues: { + displaySorting: props?.displaySorting || false, + defaultSorting: props?.defaultSorting || 'createdAt_desc', + sorting: props?.sorting || [], + }, }); - useEffect(() => { - form.reset(defaults()); - }, [widget]); - return (
@@ -117,19 +76,7 @@ export default function WidgetResourceOverviewSorting() { render={({ field }) => ( Sorteeropties weergeven - + {YesNoSelect(field, props)} )} @@ -139,9 +86,7 @@ export default function WidgetResourceOverviewSorting() { name="defaultSorting" render={({ field }) => ( - - Selecteer de standaard manier van sorteren. - + Standaard manier van sorteren. @@ -171,25 +112,35 @@ export default function WidgetResourceOverviewSorting() { Selecteer uw gewenste sorteeropties
- {sorting.map((item) => ( + {SortingTypes.map((item) => ( { return ( el.value === item.value + ) > -1 + } onCheckedChange={(checked: any) => { return checked - ? field.onChange([...field.value, item.id]) + ? field.onChange([ + ...field.value, + { + value: item.value, + label: item.label, + }, + ]) : field.onChange( field.value?.filter( - (value) => value !== item.id + (val) => val.value !== item.value ) ); }} diff --git a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/tags.tsx b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/tags.tsx index ed2186211..e02b2099d 100644 --- a/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/tags.tsx +++ b/apps/admin-server/src/pages/projects/[project]/widgets/resourceoverview/[id]/tags.tsx @@ -8,60 +8,71 @@ import { FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; + +import { Checkbox } from '@/components/ui/checkbox'; import { Separator } from '@/components/ui/separator'; import { Heading } from '@/components/ui/typography'; -import { useWidgetConfig } from '@/hooks/use-widget-config'; +import useTags from '@/hooks/use-tags'; +import { YesNoSelect } from '@/lib/form-widget-helpers'; +import { EditFieldProps } from '@/lib/form-widget-helpers/EditFieldProps'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useEffect } from 'react'; +import { ResourceOverviewWidgetProps } from '@openstad/resource-overview/src/resource-overview'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; +import { useEffect, useState } from 'react'; +import _ from 'lodash'; const formSchema = z.object({ displayTagFilters: z.boolean(), - displayType: z.string().optional(), - displayGroupType: z.boolean(), + tagGroups: z + .array( + z.object({ + type: z.string(), + label: z.string().optional(), + multiple: z.boolean(), + }) + ) + .refine((value) => value.some((item) => item), { + message: 'You have to select at least one item.', + }), + displayTagGroupName: z.boolean(), }); -export default function WidgetResourceOverviewTags() { - type FormData = z.infer; - const category = 'tags'; +type Tag = { + id: number; + name: string; + type: string; +}; - const { - data: widget, - isLoading: isLoadingWidget, - updateConfig, - } = useWidgetConfig(); +export default function WidgetResourceOverviewTags( + props: ResourceOverviewWidgetProps & + EditFieldProps +) { + type FormData = z.infer; + const { data: tags } = useTags(props.projectId); + const [tagGroupNames, setGroupedNames] = useState([]); - const defaults = () => ({ - displayTagFilters: widget?.config?.[category]?.displayTagFilters || false, - displayType: widget?.config?.[category]?.displayType || '', - displayGroupType: widget?.config?.[category]?.displayGroupType || false, - }); + useEffect(() => { + if (Array.isArray(tags)) { + const fetchedTags = tags as Array; + const groupNames = _.chain(fetchedTags).map('type').uniq().value(); + setGroupedNames(groupNames); + } + }, [tags]); async function onSubmit(values: FormData) { - try { - await updateConfig({ [category]: values }); - } catch (error) { - console.error('could falset update', error); - } + props.updateConfig({ ...props, ...values }); } const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: defaults(), + defaultValues: { + displayTagFilters: props?.displayTagFilters || false, + tagGroups: props.tagGroups || [], + displayTagGroupName: props?.displayTagGroupName || false, + }, }); - useEffect(() => { - form.reset(defaults()); - }, [widget]); - return (
@@ -76,64 +87,166 @@ export default function WidgetResourceOverviewTags() { render={({ field }) => ( Wordt het filteren op tags weergegeven? - - - - )} - /> - ( - - - Laat alleen de volgende tags zien (indien ingevuld): - - - - + {YesNoSelect(field, props)} )} /> + ( - - - Als er geen tag type geselecteerd is, moet de typename dan - weergegeven worden per groep? - - - + name="tagGroups" + render={() => ( + +
+ Selecteer de gewenste tag groepen +
+
+ {(tagGroupNames || []).map((groupName, index) => ( + <> + { + return ( + + + el.type === groupName + ) > -1 + } + onCheckedChange={(checked: any) => { + if (checked) { + const updatedFields = [ + ...field.value, + { + type: groupName, + multiple: false, + label: '', + }, + ]; + field.onChange(updatedFields); + props.onFieldChanged( + field.name, + updatedFields + ); + } else { + const updatedFields = field.value?.filter( + (val) => val.type !== groupName + ); + field.onChange(updatedFields); + props.onFieldChanged( + field.name, + updatedFields + ); + } + }} + /> + + + {groupName} + + + ); + }} + /> + + { + return ( + + + g.type === groupName + ) === undefined + } + onChange={(e) => { + const groups = field.value; + const existingGroup = groups[index]; + + if (existingGroup) { + existingGroup.label = e.target.value; + groups[index] = existingGroup; + field.onChange(groups); + props.onFieldChanged(field.name, groups); + } + }} + /> + + + ); + }} + /> + + { + return ( + + + g.type === groupName + ) === undefined + } + checked={ + field.value?.findIndex( + (el) => + el.type === groupName && el.multiple + ) > -1 + } + onCheckedChange={(checked: any) => { + const groups = field.value; + const existingGroup = groups[index]; + + // Safety check + if (!checked && existingGroup) { + existingGroup.multiple = checked; + groups[index] = existingGroup; + field.onChange(groups); + props.onFieldChanged(field.name, groups); + } else { + existingGroup.multiple = checked; + field.onChange(groups); + props.onFieldChanged(field.name, groups); + } + }} + /> + + + Multiple + + + ); + }} + /> + + ))} +
)} /> + diff --git a/apps/admin-server/tsconfig.json b/apps/admin-server/tsconfig.json index 51a881144..2c84166c1 100644 --- a/apps/admin-server/tsconfig.json +++ b/apps/admin-server/tsconfig.json @@ -29,6 +29,6 @@ "**/*.js", "**/*.jsx", "types/**/*.d.ts" - ], +, "src/lib/form-widget-helpers" ], "exclude": ["node_modules"] } diff --git a/apps/api-server/package.json b/apps/api-server/package.json index 9531afd5d..ca306df9f 100755 --- a/apps/api-server/package.json +++ b/apps/api-server/package.json @@ -54,7 +54,8 @@ "umzug": "^3.2.1", "ws": "^7.5.7", "@openstad-headless/comments": "file:../../packages/comments", - "@openstad-headless/likes": "file:../../packages/likes" + "@openstad-headless/likes": "file:../../packages/likes", + "@openstad-headless/resource-overview": "file:../../packages/resource-overview" }, "devDependencies": { "@types/cron": "^2.4.0", diff --git a/apps/api-server/src/routes/widget/widget-settings.js b/apps/api-server/src/routes/widget/widget-settings.js index 675b16a12..cc86c7c8c 100644 --- a/apps/api-server/src/routes/widget/widget-settings.js +++ b/apps/api-server/src/routes/widget/widget-settings.js @@ -1,25 +1,5 @@ // @todo: add all widgets module.exports = { - /*arguments: { - js: ['@openstad-headless/arguments-component/dist/arguments-component.umd.cjs'], - css: ['@openstad-headless/arguments-component/dist/style.css'], - name: 'OpenstadHeadlessArguments', - defaultConfig: { - resourceId: null, - title: '[[nr]] reacties voor', - isClosed: widget.project?.config?.arguments?.isClosed || true, - closedText: 'Het inzenden van reacties is niet langer mogelijk', - isVotingEnabled: false, - isReplyingEnabled: false, - descriptionMinLength: - widget.project?.config?.arguments?.descriptionMinLength || '2', - descriptionMaxLength: - widget.project?.config?.arguments?.descriptionMaxLength || '1000', - placeholder: 'Voer hier uw reactie in', - formIntro: '', - }, - },*/ - likes: { js: ['@openstad-headless/likes/dist/likes.iife.js'], css: ['@openstad-headless/likes/dist/style.css'], @@ -38,4 +18,13 @@ module.exports = { resourceId: null, }, }, + resourceoverview: { + js: ['@openstad-headless/resource-overview/dist/resource-overview.iife.js'], + css: ['@openstad-headless/resource-overview/dist/style.css'], + functionName: 'OpenstadHeadlessResourceOverview', + componentName: 'ResourceOverview', + defaultConfig: { + projectId: null, + }, + }, }; diff --git a/package-lock.json b/package-lock.json index f7658c97e..3681e0101 100755 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "http-proxy-middleware": "^2.0.6", "leaflet": "^1.9.4", "leaflet-draw": "^1.0.4", + "lodash": "^4.17.21", "lucide-react": "^0.276.0", "next": "^13.4.17", "next-auth": "^4.23.1", @@ -69,6 +70,7 @@ "@types/cron": "^2.4.0", "@types/leaflet": "^1.9.6", "@types/leaflet-draw": "^1.0.9", + "@types/lodash": "^4.14.202", "@types/node": "20.5.0", "@types/react": "18.2.20", "@types/react-csv": "^1.1.8", @@ -441,6 +443,7 @@ "@kubernetes/client-node": "^0.18.1", "@openstad-headless/comments": "file:../../packages/comments", "@openstad-headless/likes": "file:../../packages/likes", + "@openstad-headless/resource-overview": "file:../../packages/resource-overview", "bcrypt": "^5.0.1", "body-parser": "^1.20.2", "co": "^4.6.0", @@ -5418,6 +5421,10 @@ "resolved": "packages/likes", "link": true }, + "node_modules/@openstad-headless/resource-overview": { + "resolved": "packages/resource-overview", + "link": true + }, "node_modules/@openstad-headless/ui": { "resolved": "packages/ui", "link": true @@ -8297,6 +8304,12 @@ "@types/leaflet": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -21856,10 +21869,6 @@ "node": ">=10" } }, - "node_modules/resource-overview": { - "resolved": "packages/resource-overview", - "link": true - }, "node_modules/ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -26651,6 +26660,7 @@ } }, "packages/resource-overview": { + "name": "@openstad-headless/resource-overview", "version": "0.0.0", "dependencies": { "@openstad-headless/data-store": "^1.0.0", diff --git a/packages/data-store/src/hooks/use-resources.js b/packages/data-store/src/hooks/use-resources.js index ccd1340e6..4496ec638 100644 --- a/packages/data-store/src/hooks/use-resources.js +++ b/packages/data-store/src/hooks/use-resources.js @@ -7,7 +7,10 @@ export default function useResources(props) { const resourceId = props.resourceId || props.config?.resourceId; const sentiment = props.sentiment || props.config?.sentiment || null; - const { data, error, isLoading } = self.useSWR({ projectId }, 'resources.fetch'); + const { data, error, isLoading } = self.useSWR( + { projectId }, + 'resources.fetch' + ); // add functionality let resources = data || []; diff --git a/packages/data-store/src/index.js b/packages/data-store/src/index.js index ac2096ce3..2f02d255e 100644 --- a/packages/data-store/src/index.js +++ b/packages/data-store/src/index.js @@ -67,7 +67,8 @@ function DataStore(props = { config: {} }) { defaultOptions.populateCache = (newData, currentData) => mergeData(currentData, newData, options.action); } - if (newData.parentId) { + + if (newData?.parentId) { // currently for comments: replies are subobjects and SWR can't handle that defaultOptions.revalidate = true; } diff --git a/packages/lib/ui-helpers.tsx b/packages/lib/ui-helpers.tsx new file mode 100644 index 000000000..d167bec08 --- /dev/null +++ b/packages/lib/ui-helpers.tsx @@ -0,0 +1,6 @@ +export function elipsize(value: string, maxLength: number) { + if (value.length > maxLength) { + return value.substring(0, maxLength) + '...'; + } + return value; +} diff --git a/packages/resource-overview/package.json b/packages/resource-overview/package.json index c19d8a05c..43925adec 100644 --- a/packages/resource-overview/package.json +++ b/packages/resource-overview/package.json @@ -1,5 +1,5 @@ { - "name": "resource-overview", + "name": "@openstad-headless/resource-overview", "private": true, "version": "0.0.0", "type": "module", diff --git a/packages/resource-overview/src/filters/filters.tsx b/packages/resource-overview/src/filters/filters.tsx index 6c3fc9bc8..9ce6e3dbe 100644 --- a/packages/resource-overview/src/filters/filters.tsx +++ b/packages/resource-overview/src/filters/filters.tsx @@ -1,15 +1,10 @@ -import { - Input, - MultiSelect, - SecondaryButton, - Select, -} from '@openstad-headless/ui/src'; +import { Input, SecondaryButton, Select } from '@openstad-headless/ui/src'; import React, { useState, useEffect, useRef, createRef } from 'react'; import DataStore from '../../../components/src/data-store'; -import { BaseConfig } from '../../../generic-widget-types'; import { useDebounce } from 'rooks'; import { MultiSelectTagFilter } from './multiselect-tag-filter'; import { SelectTagFilter } from './select-tag-filter'; +import { ResourceOverviewWidgetProps } from '../resource-overview'; //Todo correctly type resources. Will be possible when the datastore is correctly typed @@ -24,31 +19,24 @@ type Filter = { type Props = { resources: any; dataStore: DataStore; - tagTypes?: Array<{ type: string; placeholder?: string; multiple?: boolean }>; onUpdateFilter?: (filter: Filter) => void; -} & BaseConfig; +} & ResourceOverviewWidgetProps; export function Filters({ resources, dataStore, - tagTypes = [ - { type: 'theme', placeholder: 'Selecteer een thema', multiple: true }, - { type: 'area', placeholder: 'Selecteer een gebied' }, - ], - sortOptions = [ - { value: 'title', label: 'Titel' }, - { value: 'createdAt_desc', label: 'Nieuwste eerst' }, - { value: 'createdAt_asc', label: 'Oudste eerst' }, - ], + sorting = [], + tagGroups = [], + onUpdateFilter, ...props }: Props) { - - const defaultFilter = { tags: {}, search: { text: '' } }; - tagTypes.forEach((tagType) => { - defaultFilter.tags[tagType.type] = null; + const defaultFilter = { tags: {}, search: { text: '' }, sort: '' }; + tagGroups.forEach((tGroup) => { + defaultFilter.tags[tGroup.type] = null; }); + console.log({ tagGroups }); const [filter, setFilter] = useState(defaultFilter); const [selectedOptions, setSelected] = useState<{}>({}); @@ -65,18 +53,28 @@ export function Filters({ useEffect(() => { // add or remove refs setElRefs((elRefs) => - Array(tagTypes.length) + Array(tagGroups.length) .fill(undefined) .map((_, i) => elRefs[i] || createRef()) ); - }, [tagTypes]); + }, [tagGroups]); + + useEffect(() => { + if (sortingRef.current && props.defaultSorting) { + const index = sorting.findIndex((s) => s.value === props.defaultSorting); + if (index > -1) { + // + 1 for the placeholder option + sortingRef.current.selectedIndex = index + 1; + } + } + }, []); function updateFilter(newFilter: Filter) { setFilter(newFilter); onUpdateFilter && onUpdateFilter(newFilter); } - function setTags(type, values) { + function setTags(type: string, values: any[]) { updateFilter({ ...filter, tags: { @@ -88,7 +86,7 @@ export function Filters({ const search = useDebounce(setSearch, 300); - function setSearch(value) { + function setSearch(value: string) { updateFilter({ ...filter, search: { @@ -97,7 +95,7 @@ export function Filters({ }); } - function setSort(value) { + function setSort(value: string) { updateFilter({ ...filter, sort: value, @@ -127,53 +125,56 @@ export function Filters({ return (
- search(e.target.value)} - className="osc-resource-overview-search" - placeholder="Zoeken" - /> - - {tagTypes.map((tagType, index) => { - if (tagType.multiple) { - return ( - - updateTagList(tagType.type, updatedTag) - } - /> - ); - } else { - return ( - - updateTagList(tagType.type, updatedTag) - } - /> - ); - } - })} - - + {props.displaySearch ? ( + search(e.target.value)} + className="osc-resource-overview-search" + placeholder="Zoeken" + /> + ) : null} + + {props.displayTagFilters ? ( + <> + {tagGroups.map((tagGroup, index) => { + if (tagGroup.multiple) { + return ( + + updateTagList(tagGroup.type, updatedTag) + } + /> + ); + } else { + return ( + + updateTagList(tagGroup.type, updatedTag) + } + /> + ); + } + })} + + ) : null} + + {props.displaySorting ? ( + + ) : null} { @@ -197,6 +198,6 @@ export function Filters({ Wis alles
-
+ ); } diff --git a/packages/resource-overview/src/filters/multiselect-tag-filter.tsx b/packages/resource-overview/src/filters/multiselect-tag-filter.tsx index d5c4bf752..ac9ac0a02 100644 --- a/packages/resource-overview/src/filters/multiselect-tag-filter.tsx +++ b/packages/resource-overview/src/filters/multiselect-tag-filter.tsx @@ -1,7 +1,7 @@ -import { MultiSelect, Select } from '@openstad-headless/ui/src'; -import React, { useState, forwardRef } from 'react'; +import { MultiSelect } from '@openstad-headless/ui/src'; +import React from 'react'; import DataStore from '../../../components/src/data-store'; -import { BaseConfig } from '../../../generic-widget-types'; +import { BaseProps } from '../../../types/base-props'; //Todo correctly type resources. Will be possible when the datastore is correctly typed @@ -11,7 +11,7 @@ type Props = { placeholder?: string; selected?: string[]; onUpdateFilter?: (filter: string) => void; -} & BaseConfig; +} & BaseProps; const MultiSelectTagFilter = ({ dataStore, @@ -32,7 +32,7 @@ const MultiSelectTagFilter = ({ onItemSelected={(value) => { onUpdateFilter && onUpdateFilter(value); }} - options={(tags || []).map((tag) => ({ + options={(tags || []).map((tag: { id: string; name: string }) => ({ value: tag.id, label: tag.name, checked: selected.includes(tag.id), diff --git a/packages/resource-overview/src/filters/select-tag-filter.tsx b/packages/resource-overview/src/filters/select-tag-filter.tsx index ba5755050..52c53cae0 100644 --- a/packages/resource-overview/src/filters/select-tag-filter.tsx +++ b/packages/resource-overview/src/filters/select-tag-filter.tsx @@ -1,7 +1,7 @@ -import { MultiSelect, Select } from '@openstad-headless/ui/src'; -import React, { useState, forwardRef } from 'react'; +import { Select } from '@openstad-headless/ui/src'; +import React, { forwardRef } from 'react'; import DataStore from '../../../components/src/data-store'; -import { BaseConfig } from '../../../generic-widget-types'; +import { BaseProps } from '../../../types/base-props'; //Todo correctly type resources. Will be possible when the datastore is correctly typed @@ -10,7 +10,7 @@ type Props = { tagType: string; placeholder?: string; onUpdateFilter?: (filter: string) => void; -} & BaseConfig; +} & BaseProps; const SelectTagFilter = forwardRef( ({ dataStore, tagType, onUpdateFilter, ...props }, ref) => { @@ -23,7 +23,7 @@ const SelectTagFilter = forwardRef( return (