From 7fe5502dc173cfee7c3b3178fb233264ad7c6dc3 Mon Sep 17 00:00:00 2001
From: Valentin Popov <valentin@popov.link>
Date: Thu, 5 Feb 2026 11:53:17 +0000
Subject: Add checkbox-list custom field plugin to Strapi

- Introduced a new custom field type 'checkbox-list' with associated input component.
- Updated package.json to reflect the new plugin name.
- Added necessary server-side files for plugin registration, including bootstrap, destroy, and service methods.
- Updated package-lock.json to include new dependencies and versions.
- Enhanced admin interface with custom field registration and input handling.
---
 admin/src/components/CheckboxListInput.tsx | 115 +++++++++++++++++++++++++++++
 1 file changed, 115 insertions(+)
 create mode 100644 admin/src/components/CheckboxListInput.tsx

(limited to 'admin/src/components/CheckboxListInput.tsx')

diff --git a/admin/src/components/CheckboxListInput.tsx b/admin/src/components/CheckboxListInput.tsx
new file mode 100644
index 0000000..6da9766
--- /dev/null
+++ b/admin/src/components/CheckboxListInput.tsx
@@ -0,0 +1,115 @@
+import type { ReactNode } from 'react';
+
+import { Box, Checkbox, Field, Flex, Typography } from '@strapi/design-system';
+import { useIntl } from 'react-intl';
+
+type CheckboxListInputProps = {
+  name: string;
+  value?: unknown;
+  onChange: (eventOrPath: { target: { name: string; value: string[] } }, value?: unknown) => void;
+  attribute?: {
+    enum?: string[];
+    options?: {
+      enum?: string[];
+    };
+  } | null;
+  label?: ReactNode;
+  hint?: ReactNode;
+  required?: boolean;
+  disabled?: boolean;
+  error?: string;
+  labelAction?: ReactNode;
+};
+
+const getEnumValues = (attribute: CheckboxListInputProps['attribute']): string[] => {
+  if (!attribute) {
+    return [];
+  }
+
+  if (Array.isArray(attribute.enum)) {
+    return attribute.enum;
+  }
+
+  if (Array.isArray(attribute.options?.enum)) {
+    return attribute.options.enum;
+  }
+
+  return [];
+};
+
+const normalizeValue = (value: unknown): string[] => {
+  if (Array.isArray(value)) {
+    return value.filter((item): item is string => typeof item === 'string');
+  }
+
+  if (typeof value === 'string' && value.length > 0) {
+    return [value];
+  }
+
+  return [];
+};
+
+const CheckboxListInput = ({
+  name,
+  value,
+  onChange,
+  attribute,
+  label,
+  hint,
+  required = false,
+  disabled = false,
+  error,
+  labelAction,
+}: CheckboxListInputProps) => {
+  const { formatMessage } = useIntl();
+  const enumValues = getEnumValues(attribute);
+  const selectedValues = normalizeValue(value);
+
+  const handleToggle = (option: string, isChecked: boolean) => {
+    const nextValues = isChecked
+      ? Array.from(new Set([...selectedValues, option]))
+      : selectedValues.filter((item) => item !== option);
+
+    onChange({
+      target: {
+        name,
+        value: nextValues,
+      },
+    });
+  };
+
+  return (
+    <Field.Root name={name} hint={hint} error={error} required={required}>
+      <Field.Label action={labelAction}>{label ?? name}</Field.Label>
+      {enumValues.length > 0 ? (
+        <Flex direction="column" gap={2} paddingTop={1}>
+          {enumValues.map((option) => (
+            <Checkbox
+              key={option}
+              checked={selectedValues.includes(option)}
+              disabled={disabled}
+              onCheckedChange={(checked: boolean | 'indeterminate') =>
+                handleToggle(option, Boolean(checked))
+              }
+            >
+              {option}
+            </Checkbox>
+          ))}
+        </Flex>
+      ) : (
+        <Box paddingTop={1}>
+          <Typography variant="pi" textColor="neutral500">
+            {formatMessage({
+              id: 'checkbox-list.field.empty',
+              defaultMessage: 'No values configured yet.',
+            })}
+          </Typography>
+        </Box>
+      )}
+      <Field.Error />
+      <Field.Hint />
+    </Field.Root>
+  );
+};
+
+export default CheckboxListInput;
-- 
cgit v1.2.3

