strapi-plugin-bulk-updater 1.3.6 → 1.3.9
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-D812YfOE.mjs → BulkUpdater-CtEymixf.mjs} +2 -2
- package/dist/_chunks/{BulkUpdater-D812YfOE.mjs.map → BulkUpdater-CtEymixf.mjs.map} +1 -1
- package/dist/_chunks/{BulkUpdater-CXDs0Dws.js → BulkUpdater-DPEJQYYa.js} +2 -2
- package/dist/_chunks/{BulkUpdater-CXDs0Dws.js.map → BulkUpdater-DPEJQYYa.js.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-BM8B-uUz.js} +2 -2
- package/dist/_chunks/{SettingsPage-BAvuOb7V.js.map → SettingsPage-BM8B-uUz.js.map} +1 -1
- package/dist/_chunks/{SettingsPage-BWo4l6Gd.mjs → SettingsPage-DLCYx2s4.mjs} +2 -2
- package/dist/_chunks/{SettingsPage-BWo4l6Gd.mjs.map → SettingsPage-DLCYx2s4.mjs.map} +1 -1
- package/dist/_chunks/{ValidationRulesTab-Dt53h-Ke.mjs → ValidationRulesTab-tT4HIeey.mjs} +99 -19
- package/dist/_chunks/ValidationRulesTab-tT4HIeey.mjs.map +1 -0
- package/dist/_chunks/{ValidationRulesTab-BLZWPGrZ.js → ValidationRulesTab-vr-y8ntX.js} +98 -18
- package/dist/_chunks/ValidationRulesTab-vr-y8ntX.js.map +1 -0
- package/dist/admin/index.js +3 -3
- package/dist/admin/index.mjs +3 -3
- package/dist/server/index.js +214 -66
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +214 -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.js
CHANGED
|
@@ -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,59 @@ 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 cleanKey(str2) {
|
|
3634
|
+
return (str2 || "").replace(/[-_]/g, "").toLowerCase().trim();
|
|
3635
|
+
}
|
|
3636
|
+
function matchesPath(normalizedPath, leafKey, targetField) {
|
|
3637
|
+
if (!targetField) return false;
|
|
3638
|
+
const pClean = cleanKey(normalizedPath);
|
|
3639
|
+
const kClean = cleanKey(leafKey);
|
|
3640
|
+
const tClean = cleanKey(targetField);
|
|
3641
|
+
if (!tClean.includes(".")) {
|
|
3642
|
+
return kClean === tClean || pClean === tClean || pClean.endsWith(`.${tClean}`);
|
|
3643
|
+
}
|
|
3644
|
+
return pClean === tClean || pClean.endsWith(`.${tClean}`) || pClean.endsWith(tClean) || pClean.includes(`.${tClean}`);
|
|
3645
|
+
}
|
|
3596
3646
|
const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
3597
3647
|
async getStore() {
|
|
3598
3648
|
return strapi2.store({
|
|
@@ -3621,6 +3671,9 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3621
3671
|
savedRule = {
|
|
3622
3672
|
...rules[index2],
|
|
3623
3673
|
...ruleData,
|
|
3674
|
+
collectionUid: ruleData.collectionUid !== void 0 ? ruleData.collectionUid : rules[index2].collectionUid || "*",
|
|
3675
|
+
includeRichTextHtml: ruleData.includeRichTextHtml !== void 0 ? Boolean(ruleData.includeRichTextHtml) : Boolean(rules[index2].includeRichTextHtml),
|
|
3676
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : rules[index2].richTextFields || [],
|
|
3624
3677
|
updatedAt: now
|
|
3625
3678
|
};
|
|
3626
3679
|
rules[index2] = savedRule;
|
|
@@ -3629,11 +3682,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3629
3682
|
id: ruleData.id,
|
|
3630
3683
|
name: ruleData.name || "New Rule",
|
|
3631
3684
|
description: ruleData.description || "",
|
|
3685
|
+
collectionUid: ruleData.collectionUid || "*",
|
|
3632
3686
|
targetField: ruleData.targetField || "headingLevel",
|
|
3633
3687
|
quotas: ruleData.quotas || { h1: 1 },
|
|
3634
3688
|
exceededAction: ruleData.exceededAction || "disable",
|
|
3635
3689
|
enabled: ruleData.enabled ?? true,
|
|
3636
3690
|
scope: ruleData.scope || "entry",
|
|
3691
|
+
includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
|
|
3692
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
|
|
3637
3693
|
createdAt: now,
|
|
3638
3694
|
updatedAt: now
|
|
3639
3695
|
};
|
|
@@ -3645,11 +3701,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3645
3701
|
id,
|
|
3646
3702
|
name: ruleData.name || "Custom Quota Rule",
|
|
3647
3703
|
description: ruleData.description || "",
|
|
3704
|
+
collectionUid: ruleData.collectionUid || "*",
|
|
3648
3705
|
targetField: ruleData.targetField || "headingLevel",
|
|
3649
3706
|
quotas: ruleData.quotas || { h1: 1 },
|
|
3650
3707
|
exceededAction: ruleData.exceededAction || "disable",
|
|
3651
3708
|
enabled: ruleData.enabled ?? true,
|
|
3652
3709
|
scope: ruleData.scope || "entry",
|
|
3710
|
+
includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
|
|
3711
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
|
|
3653
3712
|
createdAt: now,
|
|
3654
3713
|
updatedAt: now
|
|
3655
3714
|
};
|
|
@@ -3693,21 +3752,35 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3693
3752
|
let totalScanned = 0;
|
|
3694
3753
|
const allContentTypes = strapi2.contentTypes || {};
|
|
3695
3754
|
const allComponents = strapi2.components || {};
|
|
3755
|
+
function componentHasField(compUid, targetField2) {
|
|
3756
|
+
const comp = allComponents[compUid];
|
|
3757
|
+
if (!comp?.attributes) return false;
|
|
3758
|
+
if (matchesPath(compUid, compUid, targetField2)) return true;
|
|
3759
|
+
for (const [attrName, attrDef] of Object.entries(comp.attributes)) {
|
|
3760
|
+
if (matchesPath(attrName, attrName, targetField2) || matchesPath(`${compUid}.${attrName}`, attrName, targetField2)) return true;
|
|
3761
|
+
if (attrDef.type === "component" && attrDef.component) {
|
|
3762
|
+
if (componentHasField(attrDef.component, targetField2)) return true;
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
return false;
|
|
3766
|
+
}
|
|
3696
3767
|
function schemaHasField(ctSchema) {
|
|
3697
3768
|
if (!ctSchema || !ctSchema.attributes) return { hasField: false, populateAttrs: {} };
|
|
3698
3769
|
const rawAttrs = ctSchema.attributes;
|
|
3699
3770
|
const populateAttrs = {};
|
|
3700
|
-
let hasField =
|
|
3771
|
+
let hasField = false;
|
|
3701
3772
|
for (const [attrKey, attrDef] of Object.entries(rawAttrs)) {
|
|
3773
|
+
if (matchesPath(attrKey, attrKey, targetField)) {
|
|
3774
|
+
hasField = true;
|
|
3775
|
+
}
|
|
3702
3776
|
if (attrDef.type === "component" && attrDef.component) {
|
|
3703
|
-
|
|
3704
|
-
if (comp?.attributes?.[targetField]) {
|
|
3777
|
+
if (componentHasField(attrDef.component, targetField)) {
|
|
3705
3778
|
hasField = true;
|
|
3706
3779
|
populateAttrs[attrKey] = true;
|
|
3707
3780
|
}
|
|
3708
3781
|
} else if (attrDef.type === "dynamiczone" && Array.isArray(attrDef.components)) {
|
|
3709
3782
|
const matchingComp = attrDef.components.some(
|
|
3710
|
-
(compUid) =>
|
|
3783
|
+
(compUid) => componentHasField(compUid, targetField) || matchesPath(`${attrKey}.${compUid}`, compUid, targetField)
|
|
3711
3784
|
);
|
|
3712
3785
|
if (matchingComp) {
|
|
3713
3786
|
hasField = true;
|
|
@@ -3717,19 +3790,23 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3717
3790
|
}
|
|
3718
3791
|
return { hasField, populateAttrs };
|
|
3719
3792
|
}
|
|
3720
|
-
const candidateUids = Object.keys(allContentTypes).filter(
|
|
3721
|
-
(uid
|
|
3722
|
-
|
|
3793
|
+
const candidateUids = Object.keys(allContentTypes).filter((uid) => {
|
|
3794
|
+
if (!uid.startsWith("api::") || uid.includes("bulk-updater")) return false;
|
|
3795
|
+
if (rule.collectionUid && rule.collectionUid !== "*" && rule.collectionUid !== uid) {
|
|
3796
|
+
return false;
|
|
3797
|
+
}
|
|
3798
|
+
return true;
|
|
3799
|
+
});
|
|
3723
3800
|
const relevantCollections = [];
|
|
3724
3801
|
for (const uid of candidateUids) {
|
|
3725
3802
|
const ct = allContentTypes[uid];
|
|
3726
3803
|
if (!ct) continue;
|
|
3727
3804
|
const { hasField, populateAttrs } = schemaHasField(ct);
|
|
3728
|
-
if (hasField) {
|
|
3805
|
+
if (hasField || rule.collectionUid && rule.collectionUid === uid) {
|
|
3729
3806
|
relevantCollections.push({
|
|
3730
3807
|
uid,
|
|
3731
3808
|
displayName: ct.info?.displayName || uid,
|
|
3732
|
-
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs :
|
|
3809
|
+
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs : "*"
|
|
3733
3810
|
});
|
|
3734
3811
|
}
|
|
3735
3812
|
}
|
|
@@ -3746,35 +3823,73 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3746
3823
|
});
|
|
3747
3824
|
if (!entries || entries.length === 0) break;
|
|
3748
3825
|
for (const entry of entries) {
|
|
3749
|
-
let traverse = function(obj) {
|
|
3826
|
+
let traverse = function(obj, currentPath = "") {
|
|
3750
3827
|
if (!obj || typeof obj !== "object") return;
|
|
3828
|
+
const compName = typeof obj.__component === "string" ? obj.__component : "";
|
|
3751
3829
|
for (const key of Object.keys(obj)) {
|
|
3830
|
+
if (key === "__component") continue;
|
|
3752
3831
|
const val = obj[key];
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3832
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
3833
|
+
const normalizedPath = newPath.replace(/\.\d+/g, "");
|
|
3834
|
+
const compPath = compName ? (currentPath ? `${currentPath}.${compName}.${key}` : `${compName}.${key}`).replace(/\.\d+/g, "") : "";
|
|
3835
|
+
const isMatch = matchesPath(normalizedPath, key, targetField) || (compPath ? matchesPath(compPath, key, targetField) : false);
|
|
3836
|
+
if (isMatch && typeof val === "string" && val.trim()) {
|
|
3837
|
+
const lower = val.toLowerCase().trim();
|
|
3838
|
+
structuredCounts[lower] = (structuredCounts[lower] || 0) + 1;
|
|
3839
|
+
}
|
|
3840
|
+
if (rule.includeRichTextHtml) {
|
|
3841
|
+
if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
|
|
3842
|
+
const headings = extractHeadingsFromRichText(val);
|
|
3843
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
3844
|
+
if (count > 0) {
|
|
3845
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
} else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
|
|
3849
|
+
const headings = extractHeadingsFromRichText(val);
|
|
3850
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
3851
|
+
if (count > 0) {
|
|
3852
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
continue;
|
|
3856
|
+
}
|
|
3756
3857
|
}
|
|
3757
3858
|
if (typeof val === "object" && val !== null) {
|
|
3758
|
-
traverse(val);
|
|
3859
|
+
traverse(val, newPath);
|
|
3759
3860
|
}
|
|
3760
3861
|
}
|
|
3761
3862
|
};
|
|
3762
3863
|
totalScanned++;
|
|
3763
|
-
const
|
|
3864
|
+
const structuredCounts = {};
|
|
3865
|
+
const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
3764
3866
|
traverse(entry);
|
|
3765
3867
|
for (const [optKey, max] of Object.entries(quotas)) {
|
|
3766
|
-
const lowerOpt = optKey.toLowerCase();
|
|
3767
|
-
const
|
|
3768
|
-
|
|
3868
|
+
const lowerOpt = optKey.toLowerCase().trim();
|
|
3869
|
+
const structCount = structuredCounts[lowerOpt] || 0;
|
|
3870
|
+
const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerOpt] || 0 : 0;
|
|
3871
|
+
const combinedCount = structCount + rtCount;
|
|
3872
|
+
if (combinedCount > max) {
|
|
3769
3873
|
const entryLabel = entry.title || entry.name || entry.email || `Entry #${entry.documentId}`;
|
|
3874
|
+
let detail = "";
|
|
3875
|
+
if (rtCount > 0 && structCount > 0) {
|
|
3876
|
+
detail = `${structCount} structured + ${rtCount} in rich text HTML`;
|
|
3877
|
+
} else if (rtCount > 0) {
|
|
3878
|
+
detail = `${rtCount} in rich text HTML / Markdown`;
|
|
3879
|
+
} else {
|
|
3880
|
+
detail = `${structCount} in structured component`;
|
|
3881
|
+
}
|
|
3770
3882
|
violations.push({
|
|
3771
3883
|
uid,
|
|
3772
3884
|
collectionDisplayName: displayName,
|
|
3773
3885
|
documentId: entry.documentId,
|
|
3774
3886
|
entryLabel,
|
|
3775
3887
|
violatingOption: optKey.toUpperCase(),
|
|
3776
|
-
foundCount:
|
|
3888
|
+
foundCount: combinedCount,
|
|
3889
|
+
structuredCount: structCount,
|
|
3890
|
+
richTextCount: rtCount,
|
|
3777
3891
|
maxAllowed: max,
|
|
3892
|
+
detail,
|
|
3778
3893
|
editUrl: `/admin/content-manager/collection-types/${uid}/${entry.documentId}`
|
|
3779
3894
|
});
|
|
3780
3895
|
}
|
|
@@ -34049,17 +34164,41 @@ const index = {
|
|
|
34049
34164
|
if (["create", "update"].includes(context.action)) {
|
|
34050
34165
|
const data = params?.data;
|
|
34051
34166
|
if (data && typeof data === "object") {
|
|
34052
|
-
let scanDataTree = function(obj) {
|
|
34167
|
+
let scanDataTree = function(obj, currentPath = "") {
|
|
34053
34168
|
if (!obj || typeof obj !== "object") return;
|
|
34169
|
+
const compName = typeof obj.__component === "string" ? obj.__component : "";
|
|
34054
34170
|
for (const key of Object.keys(obj)) {
|
|
34171
|
+
if (key === "__component") continue;
|
|
34055
34172
|
const val = obj[key];
|
|
34056
|
-
|
|
34057
|
-
|
|
34058
|
-
|
|
34059
|
-
|
|
34173
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
34174
|
+
const normalizedPath = newPath.replace(/\.\d+/g, "");
|
|
34175
|
+
const compPath = compName ? (currentPath ? `${currentPath}.${compName}.${key}` : `${compName}.${key}`).replace(/\.\d+/g, "") : "";
|
|
34176
|
+
for (const rule of effectiveRules) {
|
|
34177
|
+
const isMatch = matchesPath(normalizedPath, key, rule.targetField) || (compPath ? matchesPath(compPath, key, rule.targetField) : false);
|
|
34178
|
+
if (isMatch && typeof val === "string" && val.trim()) {
|
|
34179
|
+
const lowerVal = val.toLowerCase().trim();
|
|
34180
|
+
structuredCounts[rule.id] = structuredCounts[rule.id] || {};
|
|
34181
|
+
structuredCounts[rule.id][lowerVal] = (structuredCounts[rule.id][lowerVal] || 0) + 1;
|
|
34182
|
+
}
|
|
34183
|
+
}
|
|
34184
|
+
if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
|
|
34185
|
+
const headings = extractHeadingsFromRichText(val);
|
|
34186
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
34187
|
+
if (count > 0) {
|
|
34188
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
34189
|
+
}
|
|
34190
|
+
}
|
|
34191
|
+
} else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
|
|
34192
|
+
const headings = extractHeadingsFromRichText(val);
|
|
34193
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
34194
|
+
if (count > 0) {
|
|
34195
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
34196
|
+
}
|
|
34197
|
+
}
|
|
34198
|
+
continue;
|
|
34060
34199
|
}
|
|
34061
34200
|
if (typeof val === "object" && val !== null) {
|
|
34062
|
-
scanDataTree(val);
|
|
34201
|
+
scanDataTree(val, newPath);
|
|
34063
34202
|
}
|
|
34064
34203
|
}
|
|
34065
34204
|
};
|
|
@@ -34073,20 +34212,22 @@ const index = {
|
|
|
34073
34212
|
} catch (err) {
|
|
34074
34213
|
strapi2.log.warn("[bulk-updater] Failed to fetch active UI validation rules:", err);
|
|
34075
34214
|
}
|
|
34076
|
-
const
|
|
34077
|
-
|
|
34078
|
-
|
|
34079
|
-
|
|
34080
|
-
|
|
34081
|
-
|
|
34082
|
-
}
|
|
34083
|
-
|
|
34084
|
-
|
|
34215
|
+
const effectiveRules = [
|
|
34216
|
+
...activeUiRules.filter((r) => !r.collectionUid || r.collectionUid === "*" || r.collectionUid === context.uid).map((r) => ({
|
|
34217
|
+
id: r.id || r.targetField,
|
|
34218
|
+
name: r.name || r.targetField,
|
|
34219
|
+
collectionUid: r.collectionUid || "*",
|
|
34220
|
+
targetField: r.targetField,
|
|
34221
|
+
quotas: r.quotas || {},
|
|
34222
|
+
includeRichTextHtml: Boolean(r.includeRichTextHtml),
|
|
34223
|
+
richTextFields: r.richTextFields || []
|
|
34224
|
+
}))
|
|
34225
|
+
];
|
|
34085
34226
|
const allComponents = strapi2.components || {};
|
|
34086
34227
|
for (const comp of Object.values(allComponents)) {
|
|
34087
34228
|
const attrs = comp.attributes || {};
|
|
34088
34229
|
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") && !
|
|
34230
|
+
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
34231
|
const quotas = attrDef.options?.quotas || {};
|
|
34091
34232
|
const items = attrDef.options?.items || [];
|
|
34092
34233
|
const itemQuotas = {};
|
|
@@ -34095,35 +34236,33 @@ const index = {
|
|
|
34095
34236
|
itemQuotas[item.value] = item.maxAllowed;
|
|
34096
34237
|
}
|
|
34097
34238
|
}
|
|
34098
|
-
|
|
34099
|
-
|
|
34100
|
-
|
|
34101
|
-
|
|
34102
|
-
|
|
34103
|
-
|
|
34104
|
-
|
|
34105
|
-
|
|
34106
|
-
|
|
34107
|
-
|
|
34239
|
+
effectiveRules.push({
|
|
34240
|
+
id: `schema_${attrName}`,
|
|
34241
|
+
name: `${attrName} quota rule`,
|
|
34242
|
+
collectionUid: "*",
|
|
34243
|
+
targetField: attrName,
|
|
34244
|
+
quotas: {
|
|
34245
|
+
h1: 1,
|
|
34246
|
+
h2: 3,
|
|
34247
|
+
...itemQuotas,
|
|
34248
|
+
...quotas
|
|
34249
|
+
},
|
|
34250
|
+
includeRichTextHtml: true,
|
|
34251
|
+
richTextFields: []
|
|
34252
|
+
});
|
|
34108
34253
|
}
|
|
34109
34254
|
}
|
|
34110
34255
|
}
|
|
34111
|
-
|
|
34112
|
-
|
|
34113
|
-
quotas: { h1: 1, h2: 3 },
|
|
34114
|
-
ruleName: "Default SEO Heading Rule"
|
|
34115
|
-
};
|
|
34116
|
-
}
|
|
34117
|
-
const counts = {};
|
|
34256
|
+
const structuredCounts = {};
|
|
34257
|
+
const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
34118
34258
|
let dataToScan = data;
|
|
34119
34259
|
if (context.action === "update" && (params?.documentId || params?.where?.documentId)) {
|
|
34120
34260
|
const docId = params?.documentId || params?.where?.documentId;
|
|
34121
|
-
|
|
34122
|
-
if (hasQuotaFieldInPayload && context.uid) {
|
|
34261
|
+
if (context.uid && docId) {
|
|
34123
34262
|
try {
|
|
34124
|
-
const existing = await strapi2.
|
|
34125
|
-
documentId: docId,
|
|
34126
|
-
populate:
|
|
34263
|
+
const existing = await strapi2.db?.query?.(context.uid)?.findOne?.({
|
|
34264
|
+
where: { documentId: docId },
|
|
34265
|
+
populate: true
|
|
34127
34266
|
});
|
|
34128
34267
|
if (existing) {
|
|
34129
34268
|
dataToScan = { ...existing, ...data };
|
|
@@ -34133,14 +34272,23 @@ const index = {
|
|
|
34133
34272
|
}
|
|
34134
34273
|
}
|
|
34135
34274
|
scanDataTree(dataToScan);
|
|
34136
|
-
for (const
|
|
34137
|
-
|
|
34138
|
-
|
|
34139
|
-
const
|
|
34140
|
-
|
|
34141
|
-
|
|
34142
|
-
|
|
34143
|
-
|
|
34275
|
+
for (const rule of effectiveRules) {
|
|
34276
|
+
const ruleCounts = structuredCounts[rule.id] || {};
|
|
34277
|
+
for (const [quotaKey, maxAllowed] of Object.entries(rule.quotas || {})) {
|
|
34278
|
+
const lowerVal = quotaKey.toLowerCase().trim();
|
|
34279
|
+
const structCount = ruleCounts[lowerVal] || 0;
|
|
34280
|
+
const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerVal] || 0 : 0;
|
|
34281
|
+
const totalFound = structCount + rtCount;
|
|
34282
|
+
if (totalFound > maxAllowed) {
|
|
34283
|
+
let detailMsg = `Option "${quotaKey.toUpperCase()}" cannot appear more than ${maxAllowed} time(s) across this entry.`;
|
|
34284
|
+
if (rtCount > 0 && structCount > 0) {
|
|
34285
|
+
detailMsg += ` Found ${totalFound} occurrences combined: ${structCount} in structured component ('${rule.targetField}') and ${rtCount} in rich text HTML / Markdown.`;
|
|
34286
|
+
} else if (rtCount > 0) {
|
|
34287
|
+
detailMsg += ` Found ${totalFound} in rich text HTML / Markdown content.`;
|
|
34288
|
+
} else {
|
|
34289
|
+
detailMsg += ` Found ${totalFound} in structured field '${rule.targetField}'.`;
|
|
34290
|
+
}
|
|
34291
|
+
throw new ApplicationError(`Validation Error (${rule.name}): ${detailMsg}`);
|
|
34144
34292
|
}
|
|
34145
34293
|
}
|
|
34146
34294
|
}
|