strapi-plugin-bulk-updater 1.3.6 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/_chunks/{BulkUpdater-CXDs0Dws.js → BulkUpdater-BosdSbGF.js} +2 -2
  2. package/dist/_chunks/{BulkUpdater-CXDs0Dws.js.map → BulkUpdater-BosdSbGF.js.map} +1 -1
  3. package/dist/_chunks/{BulkUpdater-D812YfOE.mjs → BulkUpdater-DUrmamj-.mjs} +2 -2
  4. package/dist/_chunks/{BulkUpdater-D812YfOE.mjs.map → BulkUpdater-DUrmamj-.mjs.map} +1 -1
  5. package/dist/_chunks/{ScopedQuotaSelectInput-ChWrQIBV.mjs → ScopedQuotaSelectInput-Cy3eEj9-.mjs} +50 -23
  6. package/dist/_chunks/ScopedQuotaSelectInput-Cy3eEj9-.mjs.map +1 -0
  7. package/dist/_chunks/{ScopedQuotaSelectInput-s4oUMLgK.js → ScopedQuotaSelectInput-CyGlYa9x.js} +50 -23
  8. package/dist/_chunks/ScopedQuotaSelectInput-CyGlYa9x.js.map +1 -0
  9. package/dist/_chunks/{SettingsPage-BAvuOb7V.js → SettingsPage-B6Ia7SVS.js} +2 -2
  10. package/dist/_chunks/{SettingsPage-BAvuOb7V.js.map → SettingsPage-B6Ia7SVS.js.map} +1 -1
  11. package/dist/_chunks/{SettingsPage-BWo4l6Gd.mjs → SettingsPage-Bdse5MP4.mjs} +2 -2
  12. package/dist/_chunks/{SettingsPage-BWo4l6Gd.mjs.map → SettingsPage-Bdse5MP4.mjs.map} +1 -1
  13. package/dist/_chunks/{ValidationRulesTab-BLZWPGrZ.js → ValidationRulesTab-BqGz1VLo.js} +83 -16
  14. package/dist/_chunks/ValidationRulesTab-BqGz1VLo.js.map +1 -0
  15. package/dist/_chunks/{ValidationRulesTab-Dt53h-Ke.mjs → ValidationRulesTab-Dqkson67.mjs} +84 -17
  16. package/dist/_chunks/ValidationRulesTab-Dqkson67.mjs.map +1 -0
  17. package/dist/admin/index.js +3 -3
  18. package/dist/admin/index.mjs +3 -3
  19. package/dist/server/index.js +202 -66
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +202 -66
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +3 -3
  24. package/dist/_chunks/ScopedQuotaSelectInput-ChWrQIBV.mjs.map +0 -1
  25. package/dist/_chunks/ScopedQuotaSelectInput-s4oUMLgK.js.map +0 -1
  26. package/dist/_chunks/ValidationRulesTab-BLZWPGrZ.js.map +0 -1
  27. package/dist/_chunks/ValidationRulesTab-Dt53h-Ke.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ValidationRulesTab-Dqkson67.mjs","sources":["../../admin/src/pages/ValidationRulesTab.tsx"],"sourcesContent":["import React, { useState, useEffect } from 'react';\nimport { useFetchClient, useNotification } from '@strapi/strapi/admin';\nimport {\n Box,\n Flex,\n Typography,\n Button,\n TextInput,\n Textarea,\n SingleSelect,\n SingleSelectOption,\n Badge,\n Table,\n Thead,\n Tbody,\n Tr,\n Th,\n Td,\n Alert,\n Checkbox,\n} from '@strapi/design-system';\nimport { Plus, Trash, Check, ArrowClockwise, Search, Download } from '@strapi/icons';\n\nexport interface ValidationRule {\n id: string;\n name: string;\n description?: string;\n collectionUid?: string;\n targetField: string;\n quotas: Record<string, number>;\n exceededAction: 'disable' | 'hide';\n enabled: boolean;\n scope: 'entry' | 'document';\n includeRichTextHtml?: boolean;\n richTextFields?: string[];\n createdAt?: string;\n updatedAt?: string;\n}\n\nexport default function ValidationRulesTab() {\n const { get, post, del } = useFetchClient();\n const { toggleNotification } = useNotification();\n\n const [rules, setRules] = useState<ValidationRule[]>([]);\n const [loading, setLoading] = useState<boolean>(true);\n const [modalOpen, setModalOpen] = useState<boolean>(false);\n const [editingRule, setEditingRule] = useState<ValidationRule | null>(null);\n\n // Available collections from backend models\n const [availableCollections, setAvailableCollections] = useState<Array<{ uid: string; displayName: string }>>([]);\n\n // Form state for creating/editing a rule\n const [formName, setFormName] = useState<string>('');\n const [formDesc, setFormDesc] = useState<string>('');\n const [formCollectionUid, setFormCollectionUid] = useState<string>('*');\n const [formField, setFormField] = useState<string>('');\n const [formAction, setFormAction] = useState<'disable' | 'hide'>('disable');\n const [formIncludeRichText, setFormIncludeRichText] = useState<boolean>(true);\n const [formQuotas, setFormQuotas] = useState<Array<{ key: string; maxAllowed: number }>>([\n { key: 'h1', maxAllowed: 1 },\n { key: 'h2', maxAllowed: 3 },\n ]);\n const [saving, setSaving] = useState<boolean>(false);\n\n // Scan modal state\n const [scanModalOpen, setScanModalOpen] = useState<boolean>(false);\n const [scanningRuleId, setScanningRuleId] = useState<string | null>(null);\n const [scanReport, setScanReport] = useState<any>(null);\n\n const fetchCollections = async () => {\n try {\n const res = await get('/bulk-updater/models');\n if (res.data?.targetUids && res.data?.metaByUid) {\n const list = res.data.targetUids.map((uid: string) => ({\n uid,\n displayName: res.data.metaByUid[uid]?.displayName || uid,\n }));\n setAvailableCollections(list);\n }\n } catch {\n // Fallback silently if models not loaded\n }\n };\n\n const handleScanRule = async (rule: ValidationRule) => {\n try {\n setScanningRuleId(rule.id);\n const res = await get(`/bulk-updater/validation-rules/${rule.id}/scan`);\n if (res.data && res.data.success) {\n setScanReport(res.data);\n setScanModalOpen(true);\n }\n } catch (err: any) {\n toggleNotification({\n type: 'danger',\n message: `Scan failed: ${err.message}`,\n });\n } finally {\n setScanningRuleId(null);\n }\n };\n\n const fetchRules = async () => {\n try {\n setLoading(true);\n const res = await get('/bulk-updater/validation-rules');\n if (res.data && res.data.success) {\n setRules(res.data.rules || []);\n }\n } catch (err: any) {\n toggleNotification({\n type: 'danger',\n message: `Failed to load rules: ${err.message}`,\n });\n } finally {\n setLoading(false);\n }\n };\n\n useEffect(() => {\n fetchRules();\n fetchCollections();\n }, []);\n\n const openCreateModal = () => {\n setEditingRule(null);\n setFormName('');\n setFormDesc('');\n setFormCollectionUid('*');\n setFormField('headingLevel');\n setFormAction('disable');\n setFormIncludeRichText(true);\n setFormQuotas([\n { key: 'h1', maxAllowed: 1 },\n { key: 'h2', maxAllowed: 3 },\n ]);\n setModalOpen(true);\n };\n\n const openEditModal = (rule: ValidationRule) => {\n setEditingRule(rule);\n setFormName(rule.name);\n setFormDesc(rule.description || '');\n setFormCollectionUid(rule.collectionUid || '*');\n setFormField(rule.targetField);\n setFormAction(rule.exceededAction || 'disable');\n setFormIncludeRichText(rule.includeRichTextHtml ?? true);\n const quotaPairs = Object.entries(rule.quotas || {}).map(([k, v]) => ({\n key: k,\n maxAllowed: v,\n }));\n setFormQuotas(quotaPairs.length > 0 ? quotaPairs : [{ key: 'h1', maxAllowed: 1 }]);\n setModalOpen(true);\n };\n\n const handleAddQuotaRow = () => {\n setFormQuotas((prev) => [...prev, { key: '', maxAllowed: 1 }]);\n };\n\n const handleRemoveQuotaRow = (idx: number) => {\n setFormQuotas((prev) => prev.filter((_, i) => i !== idx));\n };\n\n const handleQuotaChange = (idx: number, field: 'key' | 'maxAllowed', value: any) => {\n setFormQuotas((prev) =>\n prev.map((row, i) => {\n if (i === idx) {\n return {\n ...row,\n [field]: field === 'maxAllowed' ? Math.max(1, parseInt(value, 10) || 1) : String(value).trim(),\n };\n }\n return row;\n })\n );\n };\n\n const handleSave = async () => {\n if (!formName.trim() || !formField.trim()) {\n toggleNotification({\n type: 'warning',\n message: 'Rule Name and Target Field are required.',\n });\n return;\n }\n\n const quotasRecord: Record<string, number> = {};\n for (const q of formQuotas) {\n if (q.key.trim()) {\n quotasRecord[q.key.trim().toLowerCase()] = q.maxAllowed;\n }\n }\n\n if (Object.keys(quotasRecord).length === 0) {\n toggleNotification({\n type: 'warning',\n message: 'At least one quota option with a limit is required.',\n });\n return;\n }\n\n try {\n setSaving(true);\n const payload: Partial<ValidationRule> = {\n id: editingRule ? editingRule.id : undefined,\n name: formName.trim(),\n description: formDesc.trim(),\n collectionUid: formCollectionUid,\n targetField: formField.trim(),\n quotas: quotasRecord,\n exceededAction: formAction,\n enabled: editingRule ? editingRule.enabled : true,\n scope: 'entry',\n includeRichTextHtml: formIncludeRichText,\n };\n\n const res = await post('/bulk-updater/validation-rules', { rule: payload });\n if (res.data && res.data.success) {\n toggleNotification({\n type: 'success',\n message: `Rule \"${payload.name}\" saved successfully!`,\n });\n setModalOpen(false);\n fetchRules();\n }\n } catch (err: any) {\n toggleNotification({\n type: 'danger',\n message: `Failed to save rule: ${err.message}`,\n });\n } finally {\n setSaving(false);\n }\n };\n\n const handleToggle = async (rule: ValidationRule) => {\n try {\n const res = await post(`/bulk-updater/validation-rules/${rule.id}/toggle`, {\n enabled: !rule.enabled,\n });\n if (res.data && res.data.success) {\n setRules((prev) =>\n prev.map((r) => (r.id === rule.id ? { ...r, enabled: !rule.enabled } : r))\n );\n toggleNotification({\n type: 'info',\n message: `Rule \"${rule.name}\" is now ${!rule.enabled ? 'ACTIVE' : 'DISABLED'}.`,\n });\n }\n } catch (err: any) {\n toggleNotification({\n type: 'danger',\n message: `Failed to update rule status: ${err.message}`,\n });\n }\n };\n\n const handleDelete = async (ruleId: string, ruleName: string) => {\n if (!window.confirm(`Are you sure you want to delete the rule \"${ruleName}\"?`)) return;\n\n try {\n const res = await del(`/bulk-updater/validation-rules/${ruleId}`);\n if (res.data && res.data.success) {\n setRules((prev) => prev.filter((r) => r.id !== ruleId));\n toggleNotification({\n type: 'success',\n message: `Rule \"${ruleName}\" deleted.`,\n });\n }\n } catch (err: any) {\n toggleNotification({\n type: 'danger',\n message: `Failed to delete rule: ${err.message}`,\n });\n }\n };\n\n return (\n <Box>\n {/* Header Panel */}\n <Box padding={6} background=\"neutral0\" shadow=\"filterShadow\" hasRadius marginBottom={6}>\n <Flex justifyContent=\"space-between\" alignItems=\"center\" wrap=\"wrap\" gap={3}>\n <Box>\n <Typography variant=\"delta\" tag=\"h2\" fontWeight=\"bold\">\n Scoped & Cross-Component Validation Rules\n </Typography>\n <Typography variant=\"pi\" textColor=\"neutral600\" style={{ marginTop: 4 }}>\n Define collection-scoped rules and combined heading limits across structured components and free-form Rich Text / CKEditor HTML / Markdown content.\n </Typography>\n </Box>\n <Flex gap={2}>\n <Button\n variant=\"tertiary\"\n startIcon={<ArrowClockwise />}\n onClick={fetchRules}\n loading={loading}\n >\n Refresh\n </Button>\n <Button\n variant=\"default\"\n startIcon={<Plus />}\n onClick={openCreateModal}\n >\n Create New Rule\n </Button>\n </Flex>\n </Flex>\n </Box>\n\n {/* Rules Table */}\n <Box padding={6} background=\"neutral0\" shadow=\"filterShadow\" hasRadius>\n <Typography variant=\"sigma\" textColor=\"neutral600\" marginBottom={3} style={{ display: 'block' }}>\n CONFIGURED RULES ({rules.length})\n </Typography>\n\n {loading ? (\n <Typography variant=\"omega\" textColor=\"neutral500\">Loading validation rules...</Typography>\n ) : rules.length === 0 ? (\n <Alert variant=\"info\" title=\"No Rules Defined\">\n Click \"Create New Rule\" above to add your first cross-component quota rule.\n </Alert>\n ) : (\n <Table colCount={7} rowCount={rules.length + 1}>\n <Thead>\n <Tr>\n <Th><Typography variant=\"sigma\">RULE NAME & TARGET FIELD</Typography></Th>\n <Th><Typography variant=\"sigma\">COLLECTION SCOPE</Typography></Th>\n <Th><Typography variant=\"sigma\">QUOTA CONSTRAINTS</Typography></Th>\n <Th><Typography variant=\"sigma\">RICH TEXT / HTML</Typography></Th>\n <Th><Typography variant=\"sigma\">EXCEEDED ACTION</Typography></Th>\n <Th><Typography variant=\"sigma\">STATUS</Typography></Th>\n <Th style={{ textAlign: 'right' }}><Typography variant=\"sigma\">ACTIONS</Typography></Th>\n </Tr>\n </Thead>\n <Tbody>\n {rules.map((rule) => {\n const quotaKeys = Object.entries(rule.quotas || {});\n const isGlobal = !rule.collectionUid || rule.collectionUid === '*';\n const collectionName = !isGlobal\n ? (availableCollections.find((c) => c.uid === rule.collectionUid)?.displayName || rule.collectionUid)\n : 'All Collections (Global)';\n\n return (\n <Tr key={rule.id}>\n <Td style={{ minWidth: 200 }}>\n <Typography variant=\"omega\" fontWeight=\"bold\" textColor=\"neutral800\" display=\"block\">\n {rule.name}\n </Typography>\n <Typography variant=\"pi\" textColor=\"neutral500\">\n Target: <code>{rule.targetField}</code>\n </Typography>\n </Td>\n <Td style={{ minWidth: 160 }}>\n <Badge\n backgroundColor={isGlobal ? 'neutral150' : 'primary100'}\n textColor={isGlobal ? 'neutral700' : 'primary700'}\n >\n {collectionName}\n </Badge>\n </Td>\n <Td style={{ minWidth: 180 }}>\n <Flex gap={1} wrap=\"wrap\">\n {quotaKeys.map(([optVal, maxVal]) => (\n <Badge key={optVal} backgroundColor=\"neutral100\" textColor=\"primary700\">\n <strong>{optVal.toUpperCase()}</strong>: max {maxVal}\n </Badge>\n ))}\n </Flex>\n </Td>\n <Td style={{ minWidth: 140 }}>\n {rule.includeRichTextHtml ? (\n <Badge backgroundColor=\"success100\" textColor=\"success700\">\n HTML & Rich Text Active\n </Badge>\n ) : (\n <Badge backgroundColor=\"neutral150\" textColor=\"neutral600\">\n Structured Only\n </Badge>\n )}\n </Td>\n <Td style={{ minWidth: 110 }}>\n <Badge\n backgroundColor={rule.exceededAction === 'hide' ? 'secondary100' : 'warning100'}\n textColor={rule.exceededAction === 'hide' ? 'secondary700' : 'warning700'}\n >\n {rule.exceededAction.toUpperCase()}\n </Badge>\n </Td>\n <Td style={{ minWidth: 100 }}>\n <Button\n variant={rule.enabled ? 'success-light' : 'tertiary'}\n size=\"S\"\n onClick={() => handleToggle(rule)}\n >\n {rule.enabled ? '● ACTIVE' : '○ DISABLED'}\n </Button>\n </Td>\n <Td style={{ textAlign: 'right', minWidth: 260 }}>\n <Flex gap={2} justifyContent=\"flex-end\">\n <Button\n variant=\"secondary\"\n size=\"S\"\n startIcon={<Search />}\n onClick={() => handleScanRule(rule)}\n loading={scanningRuleId === rule.id}\n >\n Scan Violations\n </Button>\n <Button\n variant=\"tertiary\"\n size=\"S\"\n onClick={() => openEditModal(rule)}\n >\n Edit\n </Button>\n <Button\n variant=\"danger-light\"\n size=\"S\"\n startIcon={<Trash />}\n onClick={() => handleDelete(rule.id, rule.name)}\n >\n Delete\n </Button>\n </Flex>\n </Td>\n </Tr>\n );\n })}\n </Tbody>\n </Table>\n )}\n </Box>\n\n {/* Create / Edit Rule Modal */}\n {modalOpen && (\n <Box\n style={{\n position: 'fixed',\n inset: 0,\n backgroundColor: 'rgba(33, 33, 52, 0.65)',\n zIndex: 9999,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}\n >\n <Box\n background=\"neutral0\"\n hasRadius\n shadow=\"popupShadow\"\n padding={6}\n style={{ width: '90%', maxWidth: 640, maxHeight: '90vh', overflowY: 'auto' }}\n >\n <Typography variant=\"delta\" tag=\"h2\" fontWeight=\"bold\" marginBottom={1}>\n {editingRule ? 'Edit Validation Rule' : 'Create New Validation Rule'}\n </Typography>\n <Typography variant=\"pi\" textColor=\"neutral600\" marginBottom={4} style={{ display: 'block' }}>\n Configure collection-scoped quotas. Rules apply consistently to target properties and optionally scan rich text HTML content.\n </Typography>\n\n <Flex direction=\"column\" gap={4} alignItems=\"stretch\">\n <TextInput\n label=\"Rule Name\"\n placeholder=\"e.g. SEO Single H1 Page Rule\"\n value={formName}\n onChange={(e: any) => setFormName(e.target.value)}\n required\n />\n\n <SingleSelect\n label=\"Target Collection Type\"\n value={formCollectionUid}\n onChange={(val: any) => setFormCollectionUid(val)}\n hint=\"Scope this rule to a specific collection type, or select All Collections.\"\n >\n <SingleSelectOption value=\"*\">* (All Collections / Global)</SingleSelectOption>\n {availableCollections.map((c) => (\n <SingleSelectOption key={c.uid} value={c.uid}>\n {c.displayName} ({c.uid})\n </SingleSelectOption>\n ))}\n </SingleSelect>\n\n <TextInput\n label=\"Target Field / Component Property Path\"\n placeholder=\"e.g. headingLevel, titleStyle, or st-featured.titleHeadingStyle.titleStyle\"\n value={formField}\n onChange={(e: any) => setFormField(e.target.value)}\n hint=\"Attribute name or dot-notated hierarchy (e.g. 'titleStyle' or 'st-featured.titleHeadingStyle.titleStyle').\"\n required\n />\n\n <Box padding={3} background=\"neutral100\" hasRadius style={{ border: '1px solid #e0e0e0' }}>\n <Checkbox\n checked={formIncludeRichText}\n onCheckedChange={(checked: boolean) => setFormIncludeRichText(checked)}\n >\n <Box marginLeft={2}>\n <Typography variant=\"omega\" fontWeight=\"bold\">\n Scan & Combine Rich Text / CKEditor HTML Headings\n </Typography>\n <Typography variant=\"pi\" textColor=\"neutral600\" display=\"block\">\n When enabled, any &lt;h1&gt;..&lt;h6&gt; tags, Markdown headings (#), or Strapi Blocks inside rich text fields are counted together with structured component choices towards the quota.\n </Typography>\n </Box>\n </Checkbox>\n </Box>\n\n <Textarea\n label=\"Rule Description\"\n placeholder=\"Briefly explain what this constraint accomplishes...\"\n value={formDesc}\n onChange={(e: any) => setFormDesc(e.target.value)}\n />\n\n <SingleSelect\n label=\"When Quota Is Exceeded in Another Component\"\n value={formAction}\n onChange={(val: any) => setFormAction(val as 'disable' | 'hide')}\n >\n <SingleSelectOption value=\"disable\">\n Disable Option with Usage Badge [(1/1 used)]\n </SingleSelectOption>\n <SingleSelectOption value=\"hide\">\n Hide Option completely from dropdown menu\n </SingleSelectOption>\n </SingleSelect>\n\n {/* Quotas Editor */}\n <Box\n padding={4}\n background=\"neutral100\"\n hasRadius\n style={{ border: '1px solid #e0e0e0' }}\n >\n <Flex justifyContent=\"space-between\" alignItems=\"center\" marginBottom={3}>\n <Box>\n <Typography variant=\"pi\" fontWeight=\"bold\" textColor=\"neutral800\">\n Option Quotas (Max Allowed per Entry)\n </Typography>\n <Typography variant=\"pi\" textColor=\"neutral600\" style={{ display: 'block' }}>\n Options left unlisted will have unlimited usage.\n </Typography>\n </Box>\n <Button\n variant=\"secondary\"\n size=\"S\"\n startIcon={<Plus />}\n onClick={handleAddQuotaRow}\n >\n Add Option\n </Button>\n </Flex>\n\n <Flex direction=\"column\" gap={2}>\n {formQuotas.map((q, idx) => (\n <Flex key={idx} gap={2} alignItems=\"center\">\n <Box style={{ flex: 2 }}>\n <TextInput\n placeholder=\"Option key (e.g. h1)\"\n value={q.key}\n onChange={(e: any) => handleQuotaChange(idx, 'key', e.target.value)}\n />\n </Box>\n <Box style={{ flex: 1 }}>\n <TextInput\n type=\"number\"\n placeholder=\"Max\"\n value={String(q.maxAllowed)}\n onChange={(e: any) => handleQuotaChange(idx, 'maxAllowed', e.target.value)}\n />\n </Box>\n <Button\n variant=\"ghost\"\n size=\"S\"\n onClick={() => handleRemoveQuotaRow(idx)}\n disabled={formQuotas.length <= 1}\n >\n ✕\n </Button>\n </Flex>\n ))}\n </Flex>\n </Box>\n </Flex>\n\n {/* Modal Actions */}\n <Flex justifyContent=\"flex-end\" gap={2} marginTop={6}>\n <Button\n variant=\"tertiary\"\n onClick={() => setModalOpen(false)}\n disabled={saving}\n >\n Cancel\n </Button>\n <Button\n variant=\"default\"\n startIcon={<Check />}\n onClick={handleSave}\n loading={saving}\n >\n Save Rule\n </Button>\n </Flex>\n </Box>\n </Box>\n )}\n\n {/* Scan Results Modal */}\n {scanModalOpen && scanReport && (\n <Box\n style={{\n position: 'fixed',\n inset: 0,\n backgroundColor: 'rgba(33, 33, 52, 0.65)',\n zIndex: 9999,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}\n >\n <Box\n background=\"neutral0\"\n hasRadius\n shadow=\"popupShadow\"\n padding={6}\n style={{ width: '90%', maxWidth: 780, maxHeight: '85vh', overflowY: 'auto' }}\n >\n <Flex justifyContent=\"space-between\" alignItems=\"center\" marginBottom={4}>\n <Box>\n <Typography variant=\"beta\" tag=\"h2\" fontWeight=\"bold\">\n Rule Violation Audit: {scanReport.rule?.name}\n </Typography>\n <Typography variant=\"pi\" textColor=\"neutral600\">\n Target Field: <code>{scanReport.rule?.targetField}</code> • Total Entries Scanned: {scanReport.totalScanned}\n </Typography>\n </Box>\n <Badge\n backgroundColor={scanReport.totalViolations > 0 ? 'danger100' : 'success100'}\n textColor={scanReport.totalViolations > 0 ? 'danger700' : 'success700'}\n size=\"M\"\n >\n {scanReport.totalViolations > 0\n ? `${scanReport.totalViolations} VIOLATION(S) FOUND`\n : '100% COMPLIANT'}\n </Badge>\n </Flex>\n\n {scanReport.totalViolations === 0 ? (\n <Alert variant=\"success\" title=\"No Pre-Existing Violations Detected\" marginBottom={4}>\n All existing documents across your collections adhere strictly to the configured quota limits.\n </Alert>\n ) : (\n <Box>\n <Alert variant=\"danger\" title=\"Legacy Records Found Exceeding Quota\" marginBottom={4}>\n The following {scanReport.totalViolations} record(s) contain more instances of restricted options than currently permitted by this rule.\n </Alert>\n\n <Table colCount={4} rowCount={scanReport.violations.length + 1}>\n <Thead>\n <Tr>\n <Th><Typography variant=\"sigma\">CONTENT TYPE & ENTRY</Typography></Th>\n <Th><Typography variant=\"sigma\">RESTRICTED OPTION</Typography></Th>\n <Th><Typography variant=\"sigma\">COUNT VS LIMIT</Typography></Th>\n <Th style={{ textAlign: 'right' }}><Typography variant=\"sigma\">ACTION</Typography></Th>\n </Tr>\n </Thead>\n <Tbody>\n {scanReport.violations.map((v: any, idx: number) => (\n <Tr key={idx}>\n <Td>\n <Typography variant=\"omega\" fontWeight=\"bold\">\n {v.entryLabel}\n </Typography>\n <Typography variant=\"pi\" textColor=\"neutral500\" display=\"block\">\n Collection: {v.collectionDisplayName}\n </Typography>\n {v.detail && (\n <Typography variant=\"pi\" textColor=\"neutral600\" display=\"block\" style={{ fontSize: 11 }}>\n Breakdown: {v.detail}\n </Typography>\n )}\n </Td>\n <Td>\n <Badge backgroundColor=\"warning100\" textColor=\"warning700\">\n {v.violatingOption}\n </Badge>\n </Td>\n <Td>\n <Typography variant=\"omega\" textColor=\"danger600\" fontWeight=\"bold\">\n {v.foundCount} used\n </Typography>\n <Typography variant=\"pi\" textColor=\"neutral500\">\n {' '}(Allowed max: {v.maxAllowed})\n </Typography>\n </Td>\n <Td style={{ textAlign: 'right' }}>\n <Button\n variant=\"secondary\"\n size=\"S\"\n onClick={() => window.open(v.editUrl, '_blank')}\n >\n Open in Editor ↗\n </Button>\n </Td>\n </Tr>\n ))}\n </Tbody>\n </Table>\n </Box>\n )}\n\n <Flex justifyContent=\"space-between\" alignItems=\"center\" marginTop={6}>\n {scanReport.totalViolations > 0 ? (\n <Button\n variant=\"secondary\"\n startIcon={<Download />}\n onClick={async () => {\n try {\n const res = await get(`/bulk-updater/csv/export-violations/${scanReport.rule.id}`);\n const csvStr = res.data?.csvContent || (typeof res.data === 'string' ? res.data : '');\n if (!csvStr) {\n toggleNotification({\n type: 'warning',\n message: 'No violation data to export',\n });\n return;\n }\n const safeContent = csvStr.startsWith('\\uFEFF') ? csvStr : '\\uFEFF' + csvStr;\n const blob = new Blob([safeContent], { type: 'text/csv;charset=utf-8;' });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.setAttribute('download', `violations_${scanReport.rule.id}.csv`);\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n toggleNotification({\n type: 'success',\n message: 'Violations exported to CSV successfully',\n });\n } catch (e: any) {\n toggleNotification({\n type: 'danger',\n message: e.message || 'Failed exporting violations CSV',\n });\n }\n }}\n >\n Export Violations CSV\n </Button>\n ) : (\n <Box />\n )}\n <Button variant=\"default\" onClick={() => setScanModalOpen(false)}>\n Close Report\n </Button>\n </Flex>\n </Box>\n </Box>\n )}\n </Box>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -13,7 +13,7 @@ const index = {
13
13
  id: `${PLUGIN_ID}.plugin.name`,
14
14
  defaultMessage: "Bulk Attribute Updater"
15
15
  },
16
- Component: () => Promise.resolve().then(() => require("../_chunks/BulkUpdater-CXDs0Dws.js")),
16
+ Component: () => Promise.resolve().then(() => require("../_chunks/BulkUpdater-BosdSbGF.js")),
17
17
  permissions: [],
18
18
  position: 3
19
19
  });
@@ -24,7 +24,7 @@ const index = {
24
24
  defaultMessage: "Bulk Updater Settings"
25
25
  },
26
26
  to: `${PLUGIN_ID}`,
27
- Component: () => Promise.resolve().then(() => require("../_chunks/SettingsPage-BAvuOb7V.js")),
27
+ Component: () => Promise.resolve().then(() => require("../_chunks/SettingsPage-B6Ia7SVS.js")),
28
28
  permissions: []
29
29
  });
30
30
  app.registerPlugin({
@@ -44,7 +44,7 @@ const index = {
44
44
  defaultMessage: "Dropdown with dynamic cross-component usage quota enforcement"
45
45
  },
46
46
  components: {
47
- Input: async () => Promise.resolve().then(() => require("../_chunks/ScopedQuotaSelectInput-s4oUMLgK.js"))
47
+ Input: async () => Promise.resolve().then(() => require("../_chunks/ScopedQuotaSelectInput-CyGlYa9x.js"))
48
48
  }
49
49
  });
50
50
  },
@@ -10,7 +10,7 @@ const index = {
10
10
  id: `${PLUGIN_ID}.plugin.name`,
11
11
  defaultMessage: "Bulk Attribute Updater"
12
12
  },
13
- Component: () => import("../_chunks/BulkUpdater-D812YfOE.mjs"),
13
+ Component: () => import("../_chunks/BulkUpdater-DUrmamj-.mjs"),
14
14
  permissions: [],
15
15
  position: 3
16
16
  });
@@ -21,7 +21,7 @@ const index = {
21
21
  defaultMessage: "Bulk Updater Settings"
22
22
  },
23
23
  to: `${PLUGIN_ID}`,
24
- Component: () => import("../_chunks/SettingsPage-BWo4l6Gd.mjs"),
24
+ Component: () => import("../_chunks/SettingsPage-Bdse5MP4.mjs"),
25
25
  permissions: []
26
26
  });
27
27
  app.registerPlugin({
@@ -41,7 +41,7 @@ const index = {
41
41
  defaultMessage: "Dropdown with dynamic cross-component usage quota enforcement"
42
42
  },
43
43
  components: {
44
- Input: async () => import("../_chunks/ScopedQuotaSelectInput-ChWrQIBV.mjs")
44
+ Input: async () => import("../_chunks/ScopedQuotaSelectInput-Cy3eEj9-.mjs")
45
45
  }
46
46
  });
47
47
  },
@@ -3580,7 +3580,8 @@ const DEFAULT_RULES = [
3580
3580
  {
3581
3581
  id: "rule_seo_heading_h1",
3582
3582
  name: "SEO Single H1 Page Rule",
3583
- description: "Enforces that only one H1 heading tag can be selected across all dynamic zone heading blocks within an entry.",
3583
+ description: "Enforces that only one H1 heading tag can be selected across all dynamic zone heading blocks within an entry, combined with rich text HTML.",
3584
+ collectionUid: "*",
3584
3585
  targetField: "headingLevel",
3585
3586
  quotas: {
3586
3587
  h1: 1,
@@ -3589,10 +3590,56 @@ const DEFAULT_RULES = [
3589
3590
  exceededAction: "disable",
3590
3591
  enabled: true,
3591
3592
  scope: "entry",
3593
+ includeRichTextHtml: true,
3594
+ richTextFields: [],
3592
3595
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3593
3596
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3594
3597
  }
3595
3598
  ];
3599
+ function extractHeadingsFromRichText(content) {
3600
+ const counts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
3601
+ if (!content) return counts;
3602
+ if (Array.isArray(content)) {
3603
+ for (const node of content) {
3604
+ if (node && typeof node === "object") {
3605
+ if (node.type === "heading" && typeof node.level === "number") {
3606
+ const tag = `h${node.level}`;
3607
+ if (counts[tag] !== void 0) counts[tag]++;
3608
+ }
3609
+ if (Array.isArray(node.children)) {
3610
+ const sub = extractHeadingsFromRichText(node.children);
3611
+ for (const k of Object.keys(counts)) counts[k] += sub[k];
3612
+ }
3613
+ }
3614
+ }
3615
+ return counts;
3616
+ }
3617
+ if (typeof content === "string") {
3618
+ for (let level = 1; level <= 6; level++) {
3619
+ const tag = `h${level}`;
3620
+ const htmlMatches = content.match(new RegExp(`<${tag}\\b[^>]*>`, "gi"));
3621
+ if (htmlMatches) {
3622
+ counts[tag] += htmlMatches.length;
3623
+ }
3624
+ const mdRegex = new RegExp(`(?:^|\\n)${"#".repeat(level)}\\s+[^\\n]+`, "g");
3625
+ const mdMatches = content.match(mdRegex);
3626
+ if (mdMatches) {
3627
+ counts[tag] += mdMatches.length;
3628
+ }
3629
+ }
3630
+ }
3631
+ return counts;
3632
+ }
3633
+ function matchesPath(normalizedPath, leafKey, targetField) {
3634
+ if (!targetField) return false;
3635
+ const pLower = (normalizedPath || "").toLowerCase().trim();
3636
+ const kLower = (leafKey || "").toLowerCase().trim();
3637
+ const tLower = targetField.toLowerCase().trim();
3638
+ if (!tLower.includes(".")) {
3639
+ return kLower === tLower || pLower === tLower || pLower.endsWith(`.${tLower}`);
3640
+ }
3641
+ return pLower === tLower || pLower.endsWith(`.${tLower}`) || pLower.endsWith(tLower);
3642
+ }
3596
3643
  const validationRulesService = ({ strapi: strapi2 }) => ({
3597
3644
  async getStore() {
3598
3645
  return strapi2.store({
@@ -3621,6 +3668,9 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
3621
3668
  savedRule = {
3622
3669
  ...rules[index2],
3623
3670
  ...ruleData,
3671
+ collectionUid: ruleData.collectionUid !== void 0 ? ruleData.collectionUid : rules[index2].collectionUid || "*",
3672
+ includeRichTextHtml: ruleData.includeRichTextHtml !== void 0 ? Boolean(ruleData.includeRichTextHtml) : Boolean(rules[index2].includeRichTextHtml),
3673
+ richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : rules[index2].richTextFields || [],
3624
3674
  updatedAt: now
3625
3675
  };
3626
3676
  rules[index2] = savedRule;
@@ -3629,11 +3679,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
3629
3679
  id: ruleData.id,
3630
3680
  name: ruleData.name || "New Rule",
3631
3681
  description: ruleData.description || "",
3682
+ collectionUid: ruleData.collectionUid || "*",
3632
3683
  targetField: ruleData.targetField || "headingLevel",
3633
3684
  quotas: ruleData.quotas || { h1: 1 },
3634
3685
  exceededAction: ruleData.exceededAction || "disable",
3635
3686
  enabled: ruleData.enabled ?? true,
3636
3687
  scope: ruleData.scope || "entry",
3688
+ includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
3689
+ richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
3637
3690
  createdAt: now,
3638
3691
  updatedAt: now
3639
3692
  };
@@ -3645,11 +3698,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
3645
3698
  id,
3646
3699
  name: ruleData.name || "Custom Quota Rule",
3647
3700
  description: ruleData.description || "",
3701
+ collectionUid: ruleData.collectionUid || "*",
3648
3702
  targetField: ruleData.targetField || "headingLevel",
3649
3703
  quotas: ruleData.quotas || { h1: 1 },
3650
3704
  exceededAction: ruleData.exceededAction || "disable",
3651
3705
  enabled: ruleData.enabled ?? true,
3652
3706
  scope: ruleData.scope || "entry",
3707
+ includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
3708
+ richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
3653
3709
  createdAt: now,
3654
3710
  updatedAt: now
3655
3711
  };
@@ -3693,21 +3749,34 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
3693
3749
  let totalScanned = 0;
3694
3750
  const allContentTypes = strapi2.contentTypes || {};
3695
3751
  const allComponents = strapi2.components || {};
3752
+ function componentHasField(compUid, targetField2) {
3753
+ const comp = allComponents[compUid];
3754
+ if (!comp?.attributes) return false;
3755
+ for (const [attrName, attrDef] of Object.entries(comp.attributes)) {
3756
+ if (matchesPath(attrName, attrName, targetField2)) return true;
3757
+ if (attrDef.type === "component" && attrDef.component) {
3758
+ if (componentHasField(attrDef.component, targetField2)) return true;
3759
+ }
3760
+ }
3761
+ return false;
3762
+ }
3696
3763
  function schemaHasField(ctSchema) {
3697
3764
  if (!ctSchema || !ctSchema.attributes) return { hasField: false, populateAttrs: {} };
3698
3765
  const rawAttrs = ctSchema.attributes;
3699
3766
  const populateAttrs = {};
3700
- let hasField = Boolean(rawAttrs[targetField]);
3767
+ let hasField = false;
3701
3768
  for (const [attrKey, attrDef] of Object.entries(rawAttrs)) {
3769
+ if (matchesPath(attrKey, attrKey, targetField)) {
3770
+ hasField = true;
3771
+ }
3702
3772
  if (attrDef.type === "component" && attrDef.component) {
3703
- const comp = allComponents[attrDef.component];
3704
- if (comp?.attributes?.[targetField]) {
3773
+ if (componentHasField(attrDef.component, targetField)) {
3705
3774
  hasField = true;
3706
3775
  populateAttrs[attrKey] = true;
3707
3776
  }
3708
3777
  } else if (attrDef.type === "dynamiczone" && Array.isArray(attrDef.components)) {
3709
3778
  const matchingComp = attrDef.components.some(
3710
- (compUid) => allComponents[compUid]?.attributes?.[targetField]
3779
+ (compUid) => componentHasField(compUid, targetField)
3711
3780
  );
3712
3781
  if (matchingComp) {
3713
3782
  hasField = true;
@@ -3717,19 +3786,23 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
3717
3786
  }
3718
3787
  return { hasField, populateAttrs };
3719
3788
  }
3720
- const candidateUids = Object.keys(allContentTypes).filter(
3721
- (uid) => uid.startsWith("api::") && !uid.includes("bulk-updater")
3722
- );
3789
+ const candidateUids = Object.keys(allContentTypes).filter((uid) => {
3790
+ if (!uid.startsWith("api::") || uid.includes("bulk-updater")) return false;
3791
+ if (rule.collectionUid && rule.collectionUid !== "*" && rule.collectionUid !== uid) {
3792
+ return false;
3793
+ }
3794
+ return true;
3795
+ });
3723
3796
  const relevantCollections = [];
3724
3797
  for (const uid of candidateUids) {
3725
3798
  const ct = allContentTypes[uid];
3726
3799
  if (!ct) continue;
3727
3800
  const { hasField, populateAttrs } = schemaHasField(ct);
3728
- if (hasField) {
3801
+ if (hasField || rule.collectionUid && rule.collectionUid === uid) {
3729
3802
  relevantCollections.push({
3730
3803
  uid,
3731
3804
  displayName: ct.info?.displayName || uid,
3732
- populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs : void 0
3805
+ populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs : "*"
3733
3806
  });
3734
3807
  }
3735
3808
  }
@@ -3746,35 +3819,69 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
3746
3819
  });
3747
3820
  if (!entries || entries.length === 0) break;
3748
3821
  for (const entry of entries) {
3749
- let traverse = function(obj) {
3822
+ let traverse = function(obj, currentPath = "") {
3750
3823
  if (!obj || typeof obj !== "object") return;
3751
3824
  for (const key of Object.keys(obj)) {
3752
3825
  const val = obj[key];
3753
- if (key === targetField && typeof val === "string" && val) {
3754
- const lower = val.toLowerCase();
3755
- counts[lower] = (counts[lower] || 0) + 1;
3826
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
3827
+ const normalizedPath = newPath.replace(/\.\d+/g, "");
3828
+ if (matchesPath(normalizedPath, key, targetField) && typeof val === "string" && val.trim()) {
3829
+ const lower = val.toLowerCase().trim();
3830
+ structuredCounts[lower] = (structuredCounts[lower] || 0) + 1;
3831
+ }
3832
+ if (rule.includeRichTextHtml) {
3833
+ if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
3834
+ const headings = extractHeadingsFromRichText(val);
3835
+ for (const [tag, count] of Object.entries(headings)) {
3836
+ if (count > 0) {
3837
+ richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
3838
+ }
3839
+ }
3840
+ } else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
3841
+ const headings = extractHeadingsFromRichText(val);
3842
+ for (const [tag, count] of Object.entries(headings)) {
3843
+ if (count > 0) {
3844
+ richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
3845
+ }
3846
+ }
3847
+ continue;
3848
+ }
3756
3849
  }
3757
3850
  if (typeof val === "object" && val !== null) {
3758
- traverse(val);
3851
+ traverse(val, newPath);
3759
3852
  }
3760
3853
  }
3761
3854
  };
3762
3855
  totalScanned++;
3763
- const counts = {};
3856
+ const structuredCounts = {};
3857
+ const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
3764
3858
  traverse(entry);
3765
3859
  for (const [optKey, max] of Object.entries(quotas)) {
3766
- const lowerOpt = optKey.toLowerCase();
3767
- const actualCount = counts[lowerOpt] || 0;
3768
- if (actualCount > max) {
3860
+ const lowerOpt = optKey.toLowerCase().trim();
3861
+ const structCount = structuredCounts[lowerOpt] || 0;
3862
+ const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerOpt] || 0 : 0;
3863
+ const combinedCount = structCount + rtCount;
3864
+ if (combinedCount > max) {
3769
3865
  const entryLabel = entry.title || entry.name || entry.email || `Entry #${entry.documentId}`;
3866
+ let detail = "";
3867
+ if (rtCount > 0 && structCount > 0) {
3868
+ detail = `${structCount} structured + ${rtCount} in rich text HTML`;
3869
+ } else if (rtCount > 0) {
3870
+ detail = `${rtCount} in rich text HTML / Markdown`;
3871
+ } else {
3872
+ detail = `${structCount} in structured component`;
3873
+ }
3770
3874
  violations.push({
3771
3875
  uid,
3772
3876
  collectionDisplayName: displayName,
3773
3877
  documentId: entry.documentId,
3774
3878
  entryLabel,
3775
3879
  violatingOption: optKey.toUpperCase(),
3776
- foundCount: actualCount,
3880
+ foundCount: combinedCount,
3881
+ structuredCount: structCount,
3882
+ richTextCount: rtCount,
3777
3883
  maxAllowed: max,
3884
+ detail,
3778
3885
  editUrl: `/admin/content-manager/collection-types/${uid}/${entry.documentId}`
3779
3886
  });
3780
3887
  }
@@ -34049,17 +34156,37 @@ const index = {
34049
34156
  if (["create", "update"].includes(context.action)) {
34050
34157
  const data = params?.data;
34051
34158
  if (data && typeof data === "object") {
34052
- let scanDataTree = function(obj) {
34159
+ let scanDataTree = function(obj, currentPath = "") {
34053
34160
  if (!obj || typeof obj !== "object") return;
34054
34161
  for (const key of Object.keys(obj)) {
34055
34162
  const val = obj[key];
34056
- if (fieldQuotas[key] && typeof val === "string" && val) {
34057
- const lowerVal = val.toLowerCase();
34058
- counts[key] = counts[key] || {};
34059
- counts[key][lowerVal] = (counts[key][lowerVal] || 0) + 1;
34163
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
34164
+ const normalizedPath = newPath.replace(/\.\d+/g, "");
34165
+ for (const rule of effectiveRules) {
34166
+ if (matchesPath(normalizedPath, key, rule.targetField) && typeof val === "string" && val.trim()) {
34167
+ const lowerVal = val.toLowerCase().trim();
34168
+ structuredCounts[rule.id] = structuredCounts[rule.id] || {};
34169
+ structuredCounts[rule.id][lowerVal] = (structuredCounts[rule.id][lowerVal] || 0) + 1;
34170
+ }
34171
+ }
34172
+ if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
34173
+ const headings = extractHeadingsFromRichText(val);
34174
+ for (const [tag, count] of Object.entries(headings)) {
34175
+ if (count > 0) {
34176
+ richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
34177
+ }
34178
+ }
34179
+ } else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
34180
+ const headings = extractHeadingsFromRichText(val);
34181
+ for (const [tag, count] of Object.entries(headings)) {
34182
+ if (count > 0) {
34183
+ richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
34184
+ }
34185
+ }
34186
+ continue;
34060
34187
  }
34061
34188
  if (typeof val === "object" && val !== null) {
34062
- scanDataTree(val);
34189
+ scanDataTree(val, newPath);
34063
34190
  }
34064
34191
  }
34065
34192
  };
@@ -34073,20 +34200,22 @@ const index = {
34073
34200
  } catch (err) {
34074
34201
  strapi2.log.warn("[bulk-updater] Failed to fetch active UI validation rules:", err);
34075
34202
  }
34076
- const fieldQuotas = {};
34077
- for (const rule of activeUiRules) {
34078
- if (rule.targetField && rule.quotas) {
34079
- fieldQuotas[rule.targetField] = {
34080
- quotas: rule.quotas,
34081
- ruleName: rule.name
34082
- };
34083
- }
34084
- }
34203
+ const effectiveRules = [
34204
+ ...activeUiRules.filter((r) => !r.collectionUid || r.collectionUid === "*" || r.collectionUid === context.uid).map((r) => ({
34205
+ id: r.id || r.targetField,
34206
+ name: r.name || r.targetField,
34207
+ collectionUid: r.collectionUid || "*",
34208
+ targetField: r.targetField,
34209
+ quotas: r.quotas || {},
34210
+ includeRichTextHtml: Boolean(r.includeRichTextHtml),
34211
+ richTextFields: r.richTextFields || []
34212
+ }))
34213
+ ];
34085
34214
  const allComponents = strapi2.components || {};
34086
34215
  for (const comp of Object.values(allComponents)) {
34087
34216
  const attrs = comp.attributes || {};
34088
34217
  for (const [attrName, attrDef] of Object.entries(attrs)) {
34089
- if (attrDef.type === "customField" && (attrDef.customField === "plugin::bulk-updater.scoped-select" || attrDef.customField === "global::scoped-select") && !fieldQuotas[attrName]) {
34218
+ if (attrDef.type === "customField" && (attrDef.customField === "plugin::bulk-updater.scoped-select" || attrDef.customField === "global::scoped-select") && !effectiveRules.some((r) => matchesPath(attrName, attrName, r.targetField))) {
34090
34219
  const quotas = attrDef.options?.quotas || {};
34091
34220
  const items = attrDef.options?.items || [];
34092
34221
  const itemQuotas = {};
@@ -34095,35 +34224,33 @@ const index = {
34095
34224
  itemQuotas[item.value] = item.maxAllowed;
34096
34225
  }
34097
34226
  }
34098
- const mergedQuotas = {
34099
- h1: 1,
34100
- h2: 3,
34101
- ...itemQuotas,
34102
- ...quotas
34103
- };
34104
- fieldQuotas[attrName] = {
34105
- quotas: mergedQuotas,
34106
- ruleName: `${attrName} quota rule`
34107
- };
34227
+ effectiveRules.push({
34228
+ id: `schema_${attrName}`,
34229
+ name: `${attrName} quota rule`,
34230
+ collectionUid: "*",
34231
+ targetField: attrName,
34232
+ quotas: {
34233
+ h1: 1,
34234
+ h2: 3,
34235
+ ...itemQuotas,
34236
+ ...quotas
34237
+ },
34238
+ includeRichTextHtml: true,
34239
+ richTextFields: []
34240
+ });
34108
34241
  }
34109
34242
  }
34110
34243
  }
34111
- if (!fieldQuotas.headingLevel) {
34112
- fieldQuotas.headingLevel = {
34113
- quotas: { h1: 1, h2: 3 },
34114
- ruleName: "Default SEO Heading Rule"
34115
- };
34116
- }
34117
- const counts = {};
34244
+ const structuredCounts = {};
34245
+ const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
34118
34246
  let dataToScan = data;
34119
34247
  if (context.action === "update" && (params?.documentId || params?.where?.documentId)) {
34120
34248
  const docId = params?.documentId || params?.where?.documentId;
34121
- const hasQuotaFieldInPayload = Object.keys(fieldQuotas).some((k) => k in data);
34122
- if (hasQuotaFieldInPayload && context.uid) {
34249
+ if (context.uid && docId) {
34123
34250
  try {
34124
- const existing = await strapi2.documents(context.uid).findOne({
34125
- documentId: docId,
34126
- populate: "*"
34251
+ const existing = await strapi2.db?.query?.(context.uid)?.findOne?.({
34252
+ where: { documentId: docId },
34253
+ populate: true
34127
34254
  });
34128
34255
  if (existing) {
34129
34256
  dataToScan = { ...existing, ...data };
@@ -34133,14 +34260,23 @@ const index = {
34133
34260
  }
34134
34261
  }
34135
34262
  scanDataTree(dataToScan);
34136
- for (const [fieldKey, ruleMeta] of Object.entries(fieldQuotas)) {
34137
- for (const [val, maxAllowed] of Object.entries(ruleMeta.quotas)) {
34138
- const lowerVal = val.toLowerCase();
34139
- const actualCount = counts[fieldKey]?.[lowerVal] || 0;
34140
- if (actualCount > maxAllowed) {
34141
- throw new ApplicationError(
34142
- `Validation Error (${ruleMeta.ruleName}): Option "${val.toUpperCase()}" on field "${fieldKey}" cannot appear more than ${maxAllowed} time(s) across this entry (found ${actualCount}).`
34143
- );
34263
+ for (const rule of effectiveRules) {
34264
+ const ruleCounts = structuredCounts[rule.id] || {};
34265
+ for (const [quotaKey, maxAllowed] of Object.entries(rule.quotas || {})) {
34266
+ const lowerVal = quotaKey.toLowerCase().trim();
34267
+ const structCount = ruleCounts[lowerVal] || 0;
34268
+ const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerVal] || 0 : 0;
34269
+ const totalFound = structCount + rtCount;
34270
+ if (totalFound > maxAllowed) {
34271
+ let detailMsg = `Option "${quotaKey.toUpperCase()}" cannot appear more than ${maxAllowed} time(s) across this entry.`;
34272
+ if (rtCount > 0 && structCount > 0) {
34273
+ detailMsg += ` Found ${totalFound} occurrences combined: ${structCount} in structured component ('${rule.targetField}') and ${rtCount} in rich text HTML / Markdown.`;
34274
+ } else if (rtCount > 0) {
34275
+ detailMsg += ` Found ${totalFound} in rich text HTML / Markdown content.`;
34276
+ } else {
34277
+ detailMsg += ` Found ${totalFound} in structured field '${rule.targetField}'.`;
34278
+ }
34279
+ throw new ApplicationError(`Validation Error (${rule.name}): ${detailMsg}`);
34144
34280
  }
34145
34281
  }
34146
34282
  }