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.
- package/dist/_chunks/{BulkUpdater-CXDs0Dws.js → BulkUpdater-BosdSbGF.js} +2 -2
- package/dist/_chunks/{BulkUpdater-CXDs0Dws.js.map → BulkUpdater-BosdSbGF.js.map} +1 -1
- package/dist/_chunks/{BulkUpdater-D812YfOE.mjs → BulkUpdater-DUrmamj-.mjs} +2 -2
- package/dist/_chunks/{BulkUpdater-D812YfOE.mjs.map → BulkUpdater-DUrmamj-.mjs.map} +1 -1
- package/dist/_chunks/{ScopedQuotaSelectInput-ChWrQIBV.mjs → ScopedQuotaSelectInput-Cy3eEj9-.mjs} +50 -23
- package/dist/_chunks/ScopedQuotaSelectInput-Cy3eEj9-.mjs.map +1 -0
- package/dist/_chunks/{ScopedQuotaSelectInput-s4oUMLgK.js → ScopedQuotaSelectInput-CyGlYa9x.js} +50 -23
- package/dist/_chunks/ScopedQuotaSelectInput-CyGlYa9x.js.map +1 -0
- package/dist/_chunks/{SettingsPage-BAvuOb7V.js → SettingsPage-B6Ia7SVS.js} +2 -2
- package/dist/_chunks/{SettingsPage-BAvuOb7V.js.map → SettingsPage-B6Ia7SVS.js.map} +1 -1
- package/dist/_chunks/{SettingsPage-BWo4l6Gd.mjs → SettingsPage-Bdse5MP4.mjs} +2 -2
- package/dist/_chunks/{SettingsPage-BWo4l6Gd.mjs.map → SettingsPage-Bdse5MP4.mjs.map} +1 -1
- package/dist/_chunks/{ValidationRulesTab-BLZWPGrZ.js → ValidationRulesTab-BqGz1VLo.js} +83 -16
- package/dist/_chunks/ValidationRulesTab-BqGz1VLo.js.map +1 -0
- package/dist/_chunks/{ValidationRulesTab-Dt53h-Ke.mjs → ValidationRulesTab-Dqkson67.mjs} +84 -17
- package/dist/_chunks/ValidationRulesTab-Dqkson67.mjs.map +1 -0
- package/dist/admin/index.js +3 -3
- package/dist/admin/index.mjs +3 -3
- package/dist/server/index.js +202 -66
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +202 -66
- package/dist/server/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/_chunks/ScopedQuotaSelectInput-ChWrQIBV.mjs.map +0 -1
- package/dist/_chunks/ScopedQuotaSelectInput-s4oUMLgK.js.map +0 -1
- package/dist/_chunks/ValidationRulesTab-BLZWPGrZ.js.map +0 -1
- package/dist/_chunks/ValidationRulesTab-Dt53h-Ke.mjs.map +0 -1
package/dist/server/index.mjs
CHANGED
|
@@ -3567,7 +3567,8 @@ const DEFAULT_RULES = [
|
|
|
3567
3567
|
{
|
|
3568
3568
|
id: "rule_seo_heading_h1",
|
|
3569
3569
|
name: "SEO Single H1 Page Rule",
|
|
3570
|
-
description: "Enforces that only one H1 heading tag can be selected across all dynamic zone heading blocks within an entry.",
|
|
3570
|
+
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.",
|
|
3571
|
+
collectionUid: "*",
|
|
3571
3572
|
targetField: "headingLevel",
|
|
3572
3573
|
quotas: {
|
|
3573
3574
|
h1: 1,
|
|
@@ -3576,10 +3577,56 @@ const DEFAULT_RULES = [
|
|
|
3576
3577
|
exceededAction: "disable",
|
|
3577
3578
|
enabled: true,
|
|
3578
3579
|
scope: "entry",
|
|
3580
|
+
includeRichTextHtml: true,
|
|
3581
|
+
richTextFields: [],
|
|
3579
3582
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3580
3583
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3581
3584
|
}
|
|
3582
3585
|
];
|
|
3586
|
+
function extractHeadingsFromRichText(content) {
|
|
3587
|
+
const counts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
3588
|
+
if (!content) return counts;
|
|
3589
|
+
if (Array.isArray(content)) {
|
|
3590
|
+
for (const node of content) {
|
|
3591
|
+
if (node && typeof node === "object") {
|
|
3592
|
+
if (node.type === "heading" && typeof node.level === "number") {
|
|
3593
|
+
const tag = `h${node.level}`;
|
|
3594
|
+
if (counts[tag] !== void 0) counts[tag]++;
|
|
3595
|
+
}
|
|
3596
|
+
if (Array.isArray(node.children)) {
|
|
3597
|
+
const sub = extractHeadingsFromRichText(node.children);
|
|
3598
|
+
for (const k of Object.keys(counts)) counts[k] += sub[k];
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
return counts;
|
|
3603
|
+
}
|
|
3604
|
+
if (typeof content === "string") {
|
|
3605
|
+
for (let level = 1; level <= 6; level++) {
|
|
3606
|
+
const tag = `h${level}`;
|
|
3607
|
+
const htmlMatches = content.match(new RegExp(`<${tag}\\b[^>]*>`, "gi"));
|
|
3608
|
+
if (htmlMatches) {
|
|
3609
|
+
counts[tag] += htmlMatches.length;
|
|
3610
|
+
}
|
|
3611
|
+
const mdRegex = new RegExp(`(?:^|\\n)${"#".repeat(level)}\\s+[^\\n]+`, "g");
|
|
3612
|
+
const mdMatches = content.match(mdRegex);
|
|
3613
|
+
if (mdMatches) {
|
|
3614
|
+
counts[tag] += mdMatches.length;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
return counts;
|
|
3619
|
+
}
|
|
3620
|
+
function matchesPath(normalizedPath, leafKey, targetField) {
|
|
3621
|
+
if (!targetField) return false;
|
|
3622
|
+
const pLower = (normalizedPath || "").toLowerCase().trim();
|
|
3623
|
+
const kLower = (leafKey || "").toLowerCase().trim();
|
|
3624
|
+
const tLower = targetField.toLowerCase().trim();
|
|
3625
|
+
if (!tLower.includes(".")) {
|
|
3626
|
+
return kLower === tLower || pLower === tLower || pLower.endsWith(`.${tLower}`);
|
|
3627
|
+
}
|
|
3628
|
+
return pLower === tLower || pLower.endsWith(`.${tLower}`) || pLower.endsWith(tLower);
|
|
3629
|
+
}
|
|
3583
3630
|
const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
3584
3631
|
async getStore() {
|
|
3585
3632
|
return strapi2.store({
|
|
@@ -3608,6 +3655,9 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3608
3655
|
savedRule = {
|
|
3609
3656
|
...rules[index2],
|
|
3610
3657
|
...ruleData,
|
|
3658
|
+
collectionUid: ruleData.collectionUid !== void 0 ? ruleData.collectionUid : rules[index2].collectionUid || "*",
|
|
3659
|
+
includeRichTextHtml: ruleData.includeRichTextHtml !== void 0 ? Boolean(ruleData.includeRichTextHtml) : Boolean(rules[index2].includeRichTextHtml),
|
|
3660
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : rules[index2].richTextFields || [],
|
|
3611
3661
|
updatedAt: now
|
|
3612
3662
|
};
|
|
3613
3663
|
rules[index2] = savedRule;
|
|
@@ -3616,11 +3666,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3616
3666
|
id: ruleData.id,
|
|
3617
3667
|
name: ruleData.name || "New Rule",
|
|
3618
3668
|
description: ruleData.description || "",
|
|
3669
|
+
collectionUid: ruleData.collectionUid || "*",
|
|
3619
3670
|
targetField: ruleData.targetField || "headingLevel",
|
|
3620
3671
|
quotas: ruleData.quotas || { h1: 1 },
|
|
3621
3672
|
exceededAction: ruleData.exceededAction || "disable",
|
|
3622
3673
|
enabled: ruleData.enabled ?? true,
|
|
3623
3674
|
scope: ruleData.scope || "entry",
|
|
3675
|
+
includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
|
|
3676
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
|
|
3624
3677
|
createdAt: now,
|
|
3625
3678
|
updatedAt: now
|
|
3626
3679
|
};
|
|
@@ -3632,11 +3685,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3632
3685
|
id,
|
|
3633
3686
|
name: ruleData.name || "Custom Quota Rule",
|
|
3634
3687
|
description: ruleData.description || "",
|
|
3688
|
+
collectionUid: ruleData.collectionUid || "*",
|
|
3635
3689
|
targetField: ruleData.targetField || "headingLevel",
|
|
3636
3690
|
quotas: ruleData.quotas || { h1: 1 },
|
|
3637
3691
|
exceededAction: ruleData.exceededAction || "disable",
|
|
3638
3692
|
enabled: ruleData.enabled ?? true,
|
|
3639
3693
|
scope: ruleData.scope || "entry",
|
|
3694
|
+
includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
|
|
3695
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
|
|
3640
3696
|
createdAt: now,
|
|
3641
3697
|
updatedAt: now
|
|
3642
3698
|
};
|
|
@@ -3680,21 +3736,34 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3680
3736
|
let totalScanned = 0;
|
|
3681
3737
|
const allContentTypes = strapi2.contentTypes || {};
|
|
3682
3738
|
const allComponents = strapi2.components || {};
|
|
3739
|
+
function componentHasField(compUid, targetField2) {
|
|
3740
|
+
const comp = allComponents[compUid];
|
|
3741
|
+
if (!comp?.attributes) return false;
|
|
3742
|
+
for (const [attrName, attrDef] of Object.entries(comp.attributes)) {
|
|
3743
|
+
if (matchesPath(attrName, attrName, targetField2)) return true;
|
|
3744
|
+
if (attrDef.type === "component" && attrDef.component) {
|
|
3745
|
+
if (componentHasField(attrDef.component, targetField2)) return true;
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
return false;
|
|
3749
|
+
}
|
|
3683
3750
|
function schemaHasField(ctSchema) {
|
|
3684
3751
|
if (!ctSchema || !ctSchema.attributes) return { hasField: false, populateAttrs: {} };
|
|
3685
3752
|
const rawAttrs = ctSchema.attributes;
|
|
3686
3753
|
const populateAttrs = {};
|
|
3687
|
-
let hasField =
|
|
3754
|
+
let hasField = false;
|
|
3688
3755
|
for (const [attrKey, attrDef] of Object.entries(rawAttrs)) {
|
|
3756
|
+
if (matchesPath(attrKey, attrKey, targetField)) {
|
|
3757
|
+
hasField = true;
|
|
3758
|
+
}
|
|
3689
3759
|
if (attrDef.type === "component" && attrDef.component) {
|
|
3690
|
-
|
|
3691
|
-
if (comp?.attributes?.[targetField]) {
|
|
3760
|
+
if (componentHasField(attrDef.component, targetField)) {
|
|
3692
3761
|
hasField = true;
|
|
3693
3762
|
populateAttrs[attrKey] = true;
|
|
3694
3763
|
}
|
|
3695
3764
|
} else if (attrDef.type === "dynamiczone" && Array.isArray(attrDef.components)) {
|
|
3696
3765
|
const matchingComp = attrDef.components.some(
|
|
3697
|
-
(compUid) =>
|
|
3766
|
+
(compUid) => componentHasField(compUid, targetField)
|
|
3698
3767
|
);
|
|
3699
3768
|
if (matchingComp) {
|
|
3700
3769
|
hasField = true;
|
|
@@ -3704,19 +3773,23 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3704
3773
|
}
|
|
3705
3774
|
return { hasField, populateAttrs };
|
|
3706
3775
|
}
|
|
3707
|
-
const candidateUids = Object.keys(allContentTypes).filter(
|
|
3708
|
-
(uid
|
|
3709
|
-
|
|
3776
|
+
const candidateUids = Object.keys(allContentTypes).filter((uid) => {
|
|
3777
|
+
if (!uid.startsWith("api::") || uid.includes("bulk-updater")) return false;
|
|
3778
|
+
if (rule.collectionUid && rule.collectionUid !== "*" && rule.collectionUid !== uid) {
|
|
3779
|
+
return false;
|
|
3780
|
+
}
|
|
3781
|
+
return true;
|
|
3782
|
+
});
|
|
3710
3783
|
const relevantCollections = [];
|
|
3711
3784
|
for (const uid of candidateUids) {
|
|
3712
3785
|
const ct = allContentTypes[uid];
|
|
3713
3786
|
if (!ct) continue;
|
|
3714
3787
|
const { hasField, populateAttrs } = schemaHasField(ct);
|
|
3715
|
-
if (hasField) {
|
|
3788
|
+
if (hasField || rule.collectionUid && rule.collectionUid === uid) {
|
|
3716
3789
|
relevantCollections.push({
|
|
3717
3790
|
uid,
|
|
3718
3791
|
displayName: ct.info?.displayName || uid,
|
|
3719
|
-
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs :
|
|
3792
|
+
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs : "*"
|
|
3720
3793
|
});
|
|
3721
3794
|
}
|
|
3722
3795
|
}
|
|
@@ -3733,35 +3806,69 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3733
3806
|
});
|
|
3734
3807
|
if (!entries || entries.length === 0) break;
|
|
3735
3808
|
for (const entry of entries) {
|
|
3736
|
-
let traverse = function(obj) {
|
|
3809
|
+
let traverse = function(obj, currentPath = "") {
|
|
3737
3810
|
if (!obj || typeof obj !== "object") return;
|
|
3738
3811
|
for (const key of Object.keys(obj)) {
|
|
3739
3812
|
const val = obj[key];
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3813
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
3814
|
+
const normalizedPath = newPath.replace(/\.\d+/g, "");
|
|
3815
|
+
if (matchesPath(normalizedPath, key, targetField) && typeof val === "string" && val.trim()) {
|
|
3816
|
+
const lower = val.toLowerCase().trim();
|
|
3817
|
+
structuredCounts[lower] = (structuredCounts[lower] || 0) + 1;
|
|
3818
|
+
}
|
|
3819
|
+
if (rule.includeRichTextHtml) {
|
|
3820
|
+
if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
|
|
3821
|
+
const headings = extractHeadingsFromRichText(val);
|
|
3822
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
3823
|
+
if (count > 0) {
|
|
3824
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
} else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
|
|
3828
|
+
const headings = extractHeadingsFromRichText(val);
|
|
3829
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
3830
|
+
if (count > 0) {
|
|
3831
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
continue;
|
|
3835
|
+
}
|
|
3743
3836
|
}
|
|
3744
3837
|
if (typeof val === "object" && val !== null) {
|
|
3745
|
-
traverse(val);
|
|
3838
|
+
traverse(val, newPath);
|
|
3746
3839
|
}
|
|
3747
3840
|
}
|
|
3748
3841
|
};
|
|
3749
3842
|
totalScanned++;
|
|
3750
|
-
const
|
|
3843
|
+
const structuredCounts = {};
|
|
3844
|
+
const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
3751
3845
|
traverse(entry);
|
|
3752
3846
|
for (const [optKey, max] of Object.entries(quotas)) {
|
|
3753
|
-
const lowerOpt = optKey.toLowerCase();
|
|
3754
|
-
const
|
|
3755
|
-
|
|
3847
|
+
const lowerOpt = optKey.toLowerCase().trim();
|
|
3848
|
+
const structCount = structuredCounts[lowerOpt] || 0;
|
|
3849
|
+
const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerOpt] || 0 : 0;
|
|
3850
|
+
const combinedCount = structCount + rtCount;
|
|
3851
|
+
if (combinedCount > max) {
|
|
3756
3852
|
const entryLabel = entry.title || entry.name || entry.email || `Entry #${entry.documentId}`;
|
|
3853
|
+
let detail = "";
|
|
3854
|
+
if (rtCount > 0 && structCount > 0) {
|
|
3855
|
+
detail = `${structCount} structured + ${rtCount} in rich text HTML`;
|
|
3856
|
+
} else if (rtCount > 0) {
|
|
3857
|
+
detail = `${rtCount} in rich text HTML / Markdown`;
|
|
3858
|
+
} else {
|
|
3859
|
+
detail = `${structCount} in structured component`;
|
|
3860
|
+
}
|
|
3757
3861
|
violations.push({
|
|
3758
3862
|
uid,
|
|
3759
3863
|
collectionDisplayName: displayName,
|
|
3760
3864
|
documentId: entry.documentId,
|
|
3761
3865
|
entryLabel,
|
|
3762
3866
|
violatingOption: optKey.toUpperCase(),
|
|
3763
|
-
foundCount:
|
|
3867
|
+
foundCount: combinedCount,
|
|
3868
|
+
structuredCount: structCount,
|
|
3869
|
+
richTextCount: rtCount,
|
|
3764
3870
|
maxAllowed: max,
|
|
3871
|
+
detail,
|
|
3765
3872
|
editUrl: `/admin/content-manager/collection-types/${uid}/${entry.documentId}`
|
|
3766
3873
|
});
|
|
3767
3874
|
}
|
|
@@ -34036,17 +34143,37 @@ const index = {
|
|
|
34036
34143
|
if (["create", "update"].includes(context.action)) {
|
|
34037
34144
|
const data = params?.data;
|
|
34038
34145
|
if (data && typeof data === "object") {
|
|
34039
|
-
let scanDataTree = function(obj) {
|
|
34146
|
+
let scanDataTree = function(obj, currentPath = "") {
|
|
34040
34147
|
if (!obj || typeof obj !== "object") return;
|
|
34041
34148
|
for (const key of Object.keys(obj)) {
|
|
34042
34149
|
const val = obj[key];
|
|
34043
|
-
|
|
34044
|
-
|
|
34045
|
-
|
|
34046
|
-
|
|
34150
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
34151
|
+
const normalizedPath = newPath.replace(/\.\d+/g, "");
|
|
34152
|
+
for (const rule of effectiveRules) {
|
|
34153
|
+
if (matchesPath(normalizedPath, key, rule.targetField) && typeof val === "string" && val.trim()) {
|
|
34154
|
+
const lowerVal = val.toLowerCase().trim();
|
|
34155
|
+
structuredCounts[rule.id] = structuredCounts[rule.id] || {};
|
|
34156
|
+
structuredCounts[rule.id][lowerVal] = (structuredCounts[rule.id][lowerVal] || 0) + 1;
|
|
34157
|
+
}
|
|
34158
|
+
}
|
|
34159
|
+
if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
|
|
34160
|
+
const headings = extractHeadingsFromRichText(val);
|
|
34161
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
34162
|
+
if (count > 0) {
|
|
34163
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
34164
|
+
}
|
|
34165
|
+
}
|
|
34166
|
+
} else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
|
|
34167
|
+
const headings = extractHeadingsFromRichText(val);
|
|
34168
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
34169
|
+
if (count > 0) {
|
|
34170
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
34171
|
+
}
|
|
34172
|
+
}
|
|
34173
|
+
continue;
|
|
34047
34174
|
}
|
|
34048
34175
|
if (typeof val === "object" && val !== null) {
|
|
34049
|
-
scanDataTree(val);
|
|
34176
|
+
scanDataTree(val, newPath);
|
|
34050
34177
|
}
|
|
34051
34178
|
}
|
|
34052
34179
|
};
|
|
@@ -34060,20 +34187,22 @@ const index = {
|
|
|
34060
34187
|
} catch (err) {
|
|
34061
34188
|
strapi2.log.warn("[bulk-updater] Failed to fetch active UI validation rules:", err);
|
|
34062
34189
|
}
|
|
34063
|
-
const
|
|
34064
|
-
|
|
34065
|
-
|
|
34066
|
-
|
|
34067
|
-
|
|
34068
|
-
|
|
34069
|
-
}
|
|
34070
|
-
|
|
34071
|
-
|
|
34190
|
+
const effectiveRules = [
|
|
34191
|
+
...activeUiRules.filter((r) => !r.collectionUid || r.collectionUid === "*" || r.collectionUid === context.uid).map((r) => ({
|
|
34192
|
+
id: r.id || r.targetField,
|
|
34193
|
+
name: r.name || r.targetField,
|
|
34194
|
+
collectionUid: r.collectionUid || "*",
|
|
34195
|
+
targetField: r.targetField,
|
|
34196
|
+
quotas: r.quotas || {},
|
|
34197
|
+
includeRichTextHtml: Boolean(r.includeRichTextHtml),
|
|
34198
|
+
richTextFields: r.richTextFields || []
|
|
34199
|
+
}))
|
|
34200
|
+
];
|
|
34072
34201
|
const allComponents = strapi2.components || {};
|
|
34073
34202
|
for (const comp of Object.values(allComponents)) {
|
|
34074
34203
|
const attrs = comp.attributes || {};
|
|
34075
34204
|
for (const [attrName, attrDef] of Object.entries(attrs)) {
|
|
34076
|
-
if (attrDef.type === "customField" && (attrDef.customField === "plugin::bulk-updater.scoped-select" || attrDef.customField === "global::scoped-select") && !
|
|
34205
|
+
if (attrDef.type === "customField" && (attrDef.customField === "plugin::bulk-updater.scoped-select" || attrDef.customField === "global::scoped-select") && !effectiveRules.some((r) => matchesPath(attrName, attrName, r.targetField))) {
|
|
34077
34206
|
const quotas = attrDef.options?.quotas || {};
|
|
34078
34207
|
const items = attrDef.options?.items || [];
|
|
34079
34208
|
const itemQuotas = {};
|
|
@@ -34082,35 +34211,33 @@ const index = {
|
|
|
34082
34211
|
itemQuotas[item.value] = item.maxAllowed;
|
|
34083
34212
|
}
|
|
34084
34213
|
}
|
|
34085
|
-
|
|
34086
|
-
|
|
34087
|
-
|
|
34088
|
-
|
|
34089
|
-
|
|
34090
|
-
|
|
34091
|
-
|
|
34092
|
-
|
|
34093
|
-
|
|
34094
|
-
|
|
34214
|
+
effectiveRules.push({
|
|
34215
|
+
id: `schema_${attrName}`,
|
|
34216
|
+
name: `${attrName} quota rule`,
|
|
34217
|
+
collectionUid: "*",
|
|
34218
|
+
targetField: attrName,
|
|
34219
|
+
quotas: {
|
|
34220
|
+
h1: 1,
|
|
34221
|
+
h2: 3,
|
|
34222
|
+
...itemQuotas,
|
|
34223
|
+
...quotas
|
|
34224
|
+
},
|
|
34225
|
+
includeRichTextHtml: true,
|
|
34226
|
+
richTextFields: []
|
|
34227
|
+
});
|
|
34095
34228
|
}
|
|
34096
34229
|
}
|
|
34097
34230
|
}
|
|
34098
|
-
|
|
34099
|
-
|
|
34100
|
-
quotas: { h1: 1, h2: 3 },
|
|
34101
|
-
ruleName: "Default SEO Heading Rule"
|
|
34102
|
-
};
|
|
34103
|
-
}
|
|
34104
|
-
const counts = {};
|
|
34231
|
+
const structuredCounts = {};
|
|
34232
|
+
const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
34105
34233
|
let dataToScan = data;
|
|
34106
34234
|
if (context.action === "update" && (params?.documentId || params?.where?.documentId)) {
|
|
34107
34235
|
const docId = params?.documentId || params?.where?.documentId;
|
|
34108
|
-
|
|
34109
|
-
if (hasQuotaFieldInPayload && context.uid) {
|
|
34236
|
+
if (context.uid && docId) {
|
|
34110
34237
|
try {
|
|
34111
|
-
const existing = await strapi2.
|
|
34112
|
-
documentId: docId,
|
|
34113
|
-
populate:
|
|
34238
|
+
const existing = await strapi2.db?.query?.(context.uid)?.findOne?.({
|
|
34239
|
+
where: { documentId: docId },
|
|
34240
|
+
populate: true
|
|
34114
34241
|
});
|
|
34115
34242
|
if (existing) {
|
|
34116
34243
|
dataToScan = { ...existing, ...data };
|
|
@@ -34120,14 +34247,23 @@ const index = {
|
|
|
34120
34247
|
}
|
|
34121
34248
|
}
|
|
34122
34249
|
scanDataTree(dataToScan);
|
|
34123
|
-
for (const
|
|
34124
|
-
|
|
34125
|
-
|
|
34126
|
-
const
|
|
34127
|
-
|
|
34128
|
-
|
|
34129
|
-
|
|
34130
|
-
|
|
34250
|
+
for (const rule of effectiveRules) {
|
|
34251
|
+
const ruleCounts = structuredCounts[rule.id] || {};
|
|
34252
|
+
for (const [quotaKey, maxAllowed] of Object.entries(rule.quotas || {})) {
|
|
34253
|
+
const lowerVal = quotaKey.toLowerCase().trim();
|
|
34254
|
+
const structCount = ruleCounts[lowerVal] || 0;
|
|
34255
|
+
const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerVal] || 0 : 0;
|
|
34256
|
+
const totalFound = structCount + rtCount;
|
|
34257
|
+
if (totalFound > maxAllowed) {
|
|
34258
|
+
let detailMsg = `Option "${quotaKey.toUpperCase()}" cannot appear more than ${maxAllowed} time(s) across this entry.`;
|
|
34259
|
+
if (rtCount > 0 && structCount > 0) {
|
|
34260
|
+
detailMsg += ` Found ${totalFound} occurrences combined: ${structCount} in structured component ('${rule.targetField}') and ${rtCount} in rich text HTML / Markdown.`;
|
|
34261
|
+
} else if (rtCount > 0) {
|
|
34262
|
+
detailMsg += ` Found ${totalFound} in rich text HTML / Markdown content.`;
|
|
34263
|
+
} else {
|
|
34264
|
+
detailMsg += ` Found ${totalFound} in structured field '${rule.targetField}'.`;
|
|
34265
|
+
}
|
|
34266
|
+
throw new ApplicationError(`Validation Error (${rule.name}): ${detailMsg}`);
|
|
34131
34267
|
}
|
|
34132
34268
|
}
|
|
34133
34269
|
}
|