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.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,59 @@ 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 cleanKey(str2) {
|
|
3621
|
+
return (str2 || "").replace(/[-_]/g, "").toLowerCase().trim();
|
|
3622
|
+
}
|
|
3623
|
+
function matchesPath(normalizedPath, leafKey, targetField) {
|
|
3624
|
+
if (!targetField) return false;
|
|
3625
|
+
const pClean = cleanKey(normalizedPath);
|
|
3626
|
+
const kClean = cleanKey(leafKey);
|
|
3627
|
+
const tClean = cleanKey(targetField);
|
|
3628
|
+
if (!tClean.includes(".")) {
|
|
3629
|
+
return kClean === tClean || pClean === tClean || pClean.endsWith(`.${tClean}`);
|
|
3630
|
+
}
|
|
3631
|
+
return pClean === tClean || pClean.endsWith(`.${tClean}`) || pClean.endsWith(tClean) || pClean.includes(`.${tClean}`);
|
|
3632
|
+
}
|
|
3583
3633
|
const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
3584
3634
|
async getStore() {
|
|
3585
3635
|
return strapi2.store({
|
|
@@ -3608,6 +3658,9 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3608
3658
|
savedRule = {
|
|
3609
3659
|
...rules[index2],
|
|
3610
3660
|
...ruleData,
|
|
3661
|
+
collectionUid: ruleData.collectionUid !== void 0 ? ruleData.collectionUid : rules[index2].collectionUid || "*",
|
|
3662
|
+
includeRichTextHtml: ruleData.includeRichTextHtml !== void 0 ? Boolean(ruleData.includeRichTextHtml) : Boolean(rules[index2].includeRichTextHtml),
|
|
3663
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : rules[index2].richTextFields || [],
|
|
3611
3664
|
updatedAt: now
|
|
3612
3665
|
};
|
|
3613
3666
|
rules[index2] = savedRule;
|
|
@@ -3616,11 +3669,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3616
3669
|
id: ruleData.id,
|
|
3617
3670
|
name: ruleData.name || "New Rule",
|
|
3618
3671
|
description: ruleData.description || "",
|
|
3672
|
+
collectionUid: ruleData.collectionUid || "*",
|
|
3619
3673
|
targetField: ruleData.targetField || "headingLevel",
|
|
3620
3674
|
quotas: ruleData.quotas || { h1: 1 },
|
|
3621
3675
|
exceededAction: ruleData.exceededAction || "disable",
|
|
3622
3676
|
enabled: ruleData.enabled ?? true,
|
|
3623
3677
|
scope: ruleData.scope || "entry",
|
|
3678
|
+
includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
|
|
3679
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
|
|
3624
3680
|
createdAt: now,
|
|
3625
3681
|
updatedAt: now
|
|
3626
3682
|
};
|
|
@@ -3632,11 +3688,14 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3632
3688
|
id,
|
|
3633
3689
|
name: ruleData.name || "Custom Quota Rule",
|
|
3634
3690
|
description: ruleData.description || "",
|
|
3691
|
+
collectionUid: ruleData.collectionUid || "*",
|
|
3635
3692
|
targetField: ruleData.targetField || "headingLevel",
|
|
3636
3693
|
quotas: ruleData.quotas || { h1: 1 },
|
|
3637
3694
|
exceededAction: ruleData.exceededAction || "disable",
|
|
3638
3695
|
enabled: ruleData.enabled ?? true,
|
|
3639
3696
|
scope: ruleData.scope || "entry",
|
|
3697
|
+
includeRichTextHtml: Boolean(ruleData.includeRichTextHtml),
|
|
3698
|
+
richTextFields: Array.isArray(ruleData.richTextFields) ? ruleData.richTextFields : [],
|
|
3640
3699
|
createdAt: now,
|
|
3641
3700
|
updatedAt: now
|
|
3642
3701
|
};
|
|
@@ -3680,21 +3739,35 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3680
3739
|
let totalScanned = 0;
|
|
3681
3740
|
const allContentTypes = strapi2.contentTypes || {};
|
|
3682
3741
|
const allComponents = strapi2.components || {};
|
|
3742
|
+
function componentHasField(compUid, targetField2) {
|
|
3743
|
+
const comp = allComponents[compUid];
|
|
3744
|
+
if (!comp?.attributes) return false;
|
|
3745
|
+
if (matchesPath(compUid, compUid, targetField2)) return true;
|
|
3746
|
+
for (const [attrName, attrDef] of Object.entries(comp.attributes)) {
|
|
3747
|
+
if (matchesPath(attrName, attrName, targetField2) || matchesPath(`${compUid}.${attrName}`, attrName, targetField2)) return true;
|
|
3748
|
+
if (attrDef.type === "component" && attrDef.component) {
|
|
3749
|
+
if (componentHasField(attrDef.component, targetField2)) return true;
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
return false;
|
|
3753
|
+
}
|
|
3683
3754
|
function schemaHasField(ctSchema) {
|
|
3684
3755
|
if (!ctSchema || !ctSchema.attributes) return { hasField: false, populateAttrs: {} };
|
|
3685
3756
|
const rawAttrs = ctSchema.attributes;
|
|
3686
3757
|
const populateAttrs = {};
|
|
3687
|
-
let hasField =
|
|
3758
|
+
let hasField = false;
|
|
3688
3759
|
for (const [attrKey, attrDef] of Object.entries(rawAttrs)) {
|
|
3760
|
+
if (matchesPath(attrKey, attrKey, targetField)) {
|
|
3761
|
+
hasField = true;
|
|
3762
|
+
}
|
|
3689
3763
|
if (attrDef.type === "component" && attrDef.component) {
|
|
3690
|
-
|
|
3691
|
-
if (comp?.attributes?.[targetField]) {
|
|
3764
|
+
if (componentHasField(attrDef.component, targetField)) {
|
|
3692
3765
|
hasField = true;
|
|
3693
3766
|
populateAttrs[attrKey] = true;
|
|
3694
3767
|
}
|
|
3695
3768
|
} else if (attrDef.type === "dynamiczone" && Array.isArray(attrDef.components)) {
|
|
3696
3769
|
const matchingComp = attrDef.components.some(
|
|
3697
|
-
(compUid) =>
|
|
3770
|
+
(compUid) => componentHasField(compUid, targetField) || matchesPath(`${attrKey}.${compUid}`, compUid, targetField)
|
|
3698
3771
|
);
|
|
3699
3772
|
if (matchingComp) {
|
|
3700
3773
|
hasField = true;
|
|
@@ -3704,19 +3777,23 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3704
3777
|
}
|
|
3705
3778
|
return { hasField, populateAttrs };
|
|
3706
3779
|
}
|
|
3707
|
-
const candidateUids = Object.keys(allContentTypes).filter(
|
|
3708
|
-
(uid
|
|
3709
|
-
|
|
3780
|
+
const candidateUids = Object.keys(allContentTypes).filter((uid) => {
|
|
3781
|
+
if (!uid.startsWith("api::") || uid.includes("bulk-updater")) return false;
|
|
3782
|
+
if (rule.collectionUid && rule.collectionUid !== "*" && rule.collectionUid !== uid) {
|
|
3783
|
+
return false;
|
|
3784
|
+
}
|
|
3785
|
+
return true;
|
|
3786
|
+
});
|
|
3710
3787
|
const relevantCollections = [];
|
|
3711
3788
|
for (const uid of candidateUids) {
|
|
3712
3789
|
const ct = allContentTypes[uid];
|
|
3713
3790
|
if (!ct) continue;
|
|
3714
3791
|
const { hasField, populateAttrs } = schemaHasField(ct);
|
|
3715
|
-
if (hasField) {
|
|
3792
|
+
if (hasField || rule.collectionUid && rule.collectionUid === uid) {
|
|
3716
3793
|
relevantCollections.push({
|
|
3717
3794
|
uid,
|
|
3718
3795
|
displayName: ct.info?.displayName || uid,
|
|
3719
|
-
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs :
|
|
3796
|
+
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs : "*"
|
|
3720
3797
|
});
|
|
3721
3798
|
}
|
|
3722
3799
|
}
|
|
@@ -3733,35 +3810,73 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
3733
3810
|
});
|
|
3734
3811
|
if (!entries || entries.length === 0) break;
|
|
3735
3812
|
for (const entry of entries) {
|
|
3736
|
-
let traverse = function(obj) {
|
|
3813
|
+
let traverse = function(obj, currentPath = "") {
|
|
3737
3814
|
if (!obj || typeof obj !== "object") return;
|
|
3815
|
+
const compName = typeof obj.__component === "string" ? obj.__component : "";
|
|
3738
3816
|
for (const key of Object.keys(obj)) {
|
|
3817
|
+
if (key === "__component") continue;
|
|
3739
3818
|
const val = obj[key];
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3819
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
3820
|
+
const normalizedPath = newPath.replace(/\.\d+/g, "");
|
|
3821
|
+
const compPath = compName ? (currentPath ? `${currentPath}.${compName}.${key}` : `${compName}.${key}`).replace(/\.\d+/g, "") : "";
|
|
3822
|
+
const isMatch = matchesPath(normalizedPath, key, targetField) || (compPath ? matchesPath(compPath, key, targetField) : false);
|
|
3823
|
+
if (isMatch && typeof val === "string" && val.trim()) {
|
|
3824
|
+
const lower = val.toLowerCase().trim();
|
|
3825
|
+
structuredCounts[lower] = (structuredCounts[lower] || 0) + 1;
|
|
3826
|
+
}
|
|
3827
|
+
if (rule.includeRichTextHtml) {
|
|
3828
|
+
if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
|
|
3829
|
+
const headings = extractHeadingsFromRichText(val);
|
|
3830
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
3831
|
+
if (count > 0) {
|
|
3832
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
} else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
|
|
3836
|
+
const headings = extractHeadingsFromRichText(val);
|
|
3837
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
3838
|
+
if (count > 0) {
|
|
3839
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
continue;
|
|
3843
|
+
}
|
|
3743
3844
|
}
|
|
3744
3845
|
if (typeof val === "object" && val !== null) {
|
|
3745
|
-
traverse(val);
|
|
3846
|
+
traverse(val, newPath);
|
|
3746
3847
|
}
|
|
3747
3848
|
}
|
|
3748
3849
|
};
|
|
3749
3850
|
totalScanned++;
|
|
3750
|
-
const
|
|
3851
|
+
const structuredCounts = {};
|
|
3852
|
+
const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
3751
3853
|
traverse(entry);
|
|
3752
3854
|
for (const [optKey, max] of Object.entries(quotas)) {
|
|
3753
|
-
const lowerOpt = optKey.toLowerCase();
|
|
3754
|
-
const
|
|
3755
|
-
|
|
3855
|
+
const lowerOpt = optKey.toLowerCase().trim();
|
|
3856
|
+
const structCount = structuredCounts[lowerOpt] || 0;
|
|
3857
|
+
const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerOpt] || 0 : 0;
|
|
3858
|
+
const combinedCount = structCount + rtCount;
|
|
3859
|
+
if (combinedCount > max) {
|
|
3756
3860
|
const entryLabel = entry.title || entry.name || entry.email || `Entry #${entry.documentId}`;
|
|
3861
|
+
let detail = "";
|
|
3862
|
+
if (rtCount > 0 && structCount > 0) {
|
|
3863
|
+
detail = `${structCount} structured + ${rtCount} in rich text HTML`;
|
|
3864
|
+
} else if (rtCount > 0) {
|
|
3865
|
+
detail = `${rtCount} in rich text HTML / Markdown`;
|
|
3866
|
+
} else {
|
|
3867
|
+
detail = `${structCount} in structured component`;
|
|
3868
|
+
}
|
|
3757
3869
|
violations.push({
|
|
3758
3870
|
uid,
|
|
3759
3871
|
collectionDisplayName: displayName,
|
|
3760
3872
|
documentId: entry.documentId,
|
|
3761
3873
|
entryLabel,
|
|
3762
3874
|
violatingOption: optKey.toUpperCase(),
|
|
3763
|
-
foundCount:
|
|
3875
|
+
foundCount: combinedCount,
|
|
3876
|
+
structuredCount: structCount,
|
|
3877
|
+
richTextCount: rtCount,
|
|
3764
3878
|
maxAllowed: max,
|
|
3879
|
+
detail,
|
|
3765
3880
|
editUrl: `/admin/content-manager/collection-types/${uid}/${entry.documentId}`
|
|
3766
3881
|
});
|
|
3767
3882
|
}
|
|
@@ -34036,17 +34151,41 @@ const index = {
|
|
|
34036
34151
|
if (["create", "update"].includes(context.action)) {
|
|
34037
34152
|
const data = params?.data;
|
|
34038
34153
|
if (data && typeof data === "object") {
|
|
34039
|
-
let scanDataTree = function(obj) {
|
|
34154
|
+
let scanDataTree = function(obj, currentPath = "") {
|
|
34040
34155
|
if (!obj || typeof obj !== "object") return;
|
|
34156
|
+
const compName = typeof obj.__component === "string" ? obj.__component : "";
|
|
34041
34157
|
for (const key of Object.keys(obj)) {
|
|
34158
|
+
if (key === "__component") continue;
|
|
34042
34159
|
const val = obj[key];
|
|
34043
|
-
|
|
34044
|
-
|
|
34045
|
-
|
|
34046
|
-
|
|
34160
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
34161
|
+
const normalizedPath = newPath.replace(/\.\d+/g, "");
|
|
34162
|
+
const compPath = compName ? (currentPath ? `${currentPath}.${compName}.${key}` : `${compName}.${key}`).replace(/\.\d+/g, "") : "";
|
|
34163
|
+
for (const rule of effectiveRules) {
|
|
34164
|
+
const isMatch = matchesPath(normalizedPath, key, rule.targetField) || (compPath ? matchesPath(compPath, key, rule.targetField) : false);
|
|
34165
|
+
if (isMatch && typeof val === "string" && val.trim()) {
|
|
34166
|
+
const lowerVal = val.toLowerCase().trim();
|
|
34167
|
+
structuredCounts[rule.id] = structuredCounts[rule.id] || {};
|
|
34168
|
+
structuredCounts[rule.id][lowerVal] = (structuredCounts[rule.id][lowerVal] || 0) + 1;
|
|
34169
|
+
}
|
|
34170
|
+
}
|
|
34171
|
+
if (typeof val === "string" && (val.includes("<h") || val.includes("#"))) {
|
|
34172
|
+
const headings = extractHeadingsFromRichText(val);
|
|
34173
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
34174
|
+
if (count > 0) {
|
|
34175
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
34176
|
+
}
|
|
34177
|
+
}
|
|
34178
|
+
} else if (Array.isArray(val) && val.length > 0 && typeof val[0] === "object" && val[0]?.type) {
|
|
34179
|
+
const headings = extractHeadingsFromRichText(val);
|
|
34180
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
34181
|
+
if (count > 0) {
|
|
34182
|
+
richTextCounts[tag] = (richTextCounts[tag] || 0) + count;
|
|
34183
|
+
}
|
|
34184
|
+
}
|
|
34185
|
+
continue;
|
|
34047
34186
|
}
|
|
34048
34187
|
if (typeof val === "object" && val !== null) {
|
|
34049
|
-
scanDataTree(val);
|
|
34188
|
+
scanDataTree(val, newPath);
|
|
34050
34189
|
}
|
|
34051
34190
|
}
|
|
34052
34191
|
};
|
|
@@ -34060,20 +34199,22 @@ const index = {
|
|
|
34060
34199
|
} catch (err) {
|
|
34061
34200
|
strapi2.log.warn("[bulk-updater] Failed to fetch active UI validation rules:", err);
|
|
34062
34201
|
}
|
|
34063
|
-
const
|
|
34064
|
-
|
|
34065
|
-
|
|
34066
|
-
|
|
34067
|
-
|
|
34068
|
-
|
|
34069
|
-
}
|
|
34070
|
-
|
|
34071
|
-
|
|
34202
|
+
const effectiveRules = [
|
|
34203
|
+
...activeUiRules.filter((r) => !r.collectionUid || r.collectionUid === "*" || r.collectionUid === context.uid).map((r) => ({
|
|
34204
|
+
id: r.id || r.targetField,
|
|
34205
|
+
name: r.name || r.targetField,
|
|
34206
|
+
collectionUid: r.collectionUid || "*",
|
|
34207
|
+
targetField: r.targetField,
|
|
34208
|
+
quotas: r.quotas || {},
|
|
34209
|
+
includeRichTextHtml: Boolean(r.includeRichTextHtml),
|
|
34210
|
+
richTextFields: r.richTextFields || []
|
|
34211
|
+
}))
|
|
34212
|
+
];
|
|
34072
34213
|
const allComponents = strapi2.components || {};
|
|
34073
34214
|
for (const comp of Object.values(allComponents)) {
|
|
34074
34215
|
const attrs = comp.attributes || {};
|
|
34075
34216
|
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") && !
|
|
34217
|
+
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
34218
|
const quotas = attrDef.options?.quotas || {};
|
|
34078
34219
|
const items = attrDef.options?.items || [];
|
|
34079
34220
|
const itemQuotas = {};
|
|
@@ -34082,35 +34223,33 @@ const index = {
|
|
|
34082
34223
|
itemQuotas[item.value] = item.maxAllowed;
|
|
34083
34224
|
}
|
|
34084
34225
|
}
|
|
34085
|
-
|
|
34086
|
-
|
|
34087
|
-
|
|
34088
|
-
|
|
34089
|
-
|
|
34090
|
-
|
|
34091
|
-
|
|
34092
|
-
|
|
34093
|
-
|
|
34094
|
-
|
|
34226
|
+
effectiveRules.push({
|
|
34227
|
+
id: `schema_${attrName}`,
|
|
34228
|
+
name: `${attrName} quota rule`,
|
|
34229
|
+
collectionUid: "*",
|
|
34230
|
+
targetField: attrName,
|
|
34231
|
+
quotas: {
|
|
34232
|
+
h1: 1,
|
|
34233
|
+
h2: 3,
|
|
34234
|
+
...itemQuotas,
|
|
34235
|
+
...quotas
|
|
34236
|
+
},
|
|
34237
|
+
includeRichTextHtml: true,
|
|
34238
|
+
richTextFields: []
|
|
34239
|
+
});
|
|
34095
34240
|
}
|
|
34096
34241
|
}
|
|
34097
34242
|
}
|
|
34098
|
-
|
|
34099
|
-
|
|
34100
|
-
quotas: { h1: 1, h2: 3 },
|
|
34101
|
-
ruleName: "Default SEO Heading Rule"
|
|
34102
|
-
};
|
|
34103
|
-
}
|
|
34104
|
-
const counts = {};
|
|
34243
|
+
const structuredCounts = {};
|
|
34244
|
+
const richTextCounts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
34105
34245
|
let dataToScan = data;
|
|
34106
34246
|
if (context.action === "update" && (params?.documentId || params?.where?.documentId)) {
|
|
34107
34247
|
const docId = params?.documentId || params?.where?.documentId;
|
|
34108
|
-
|
|
34109
|
-
if (hasQuotaFieldInPayload && context.uid) {
|
|
34248
|
+
if (context.uid && docId) {
|
|
34110
34249
|
try {
|
|
34111
|
-
const existing = await strapi2.
|
|
34112
|
-
documentId: docId,
|
|
34113
|
-
populate:
|
|
34250
|
+
const existing = await strapi2.db?.query?.(context.uid)?.findOne?.({
|
|
34251
|
+
where: { documentId: docId },
|
|
34252
|
+
populate: true
|
|
34114
34253
|
});
|
|
34115
34254
|
if (existing) {
|
|
34116
34255
|
dataToScan = { ...existing, ...data };
|
|
@@ -34120,14 +34259,23 @@ const index = {
|
|
|
34120
34259
|
}
|
|
34121
34260
|
}
|
|
34122
34261
|
scanDataTree(dataToScan);
|
|
34123
|
-
for (const
|
|
34124
|
-
|
|
34125
|
-
|
|
34126
|
-
const
|
|
34127
|
-
|
|
34128
|
-
|
|
34129
|
-
|
|
34130
|
-
|
|
34262
|
+
for (const rule of effectiveRules) {
|
|
34263
|
+
const ruleCounts = structuredCounts[rule.id] || {};
|
|
34264
|
+
for (const [quotaKey, maxAllowed] of Object.entries(rule.quotas || {})) {
|
|
34265
|
+
const lowerVal = quotaKey.toLowerCase().trim();
|
|
34266
|
+
const structCount = ruleCounts[lowerVal] || 0;
|
|
34267
|
+
const rtCount = rule.includeRichTextHtml ? richTextCounts[lowerVal] || 0 : 0;
|
|
34268
|
+
const totalFound = structCount + rtCount;
|
|
34269
|
+
if (totalFound > maxAllowed) {
|
|
34270
|
+
let detailMsg = `Option "${quotaKey.toUpperCase()}" cannot appear more than ${maxAllowed} time(s) across this entry.`;
|
|
34271
|
+
if (rtCount > 0 && structCount > 0) {
|
|
34272
|
+
detailMsg += ` Found ${totalFound} occurrences combined: ${structCount} in structured component ('${rule.targetField}') and ${rtCount} in rich text HTML / Markdown.`;
|
|
34273
|
+
} else if (rtCount > 0) {
|
|
34274
|
+
detailMsg += ` Found ${totalFound} in rich text HTML / Markdown content.`;
|
|
34275
|
+
} else {
|
|
34276
|
+
detailMsg += ` Found ${totalFound} in structured field '${rule.targetField}'.`;
|
|
34277
|
+
}
|
|
34278
|
+
throw new ApplicationError(`Validation Error (${rule.name}): ${detailMsg}`);
|
|
34131
34279
|
}
|
|
34132
34280
|
}
|
|
34133
34281
|
}
|