סקילים — ישויות ושדות
ארבעה סקילים שעובדים על שכבת הנתונים: מה קיים בבסיס הנתונים, איך שדה נראה בטופס, ואיך שדה אחד משפיע על שדה שני. שלושה מהם נשענים על מוסכמות של הפלטפורמה שאי אפשר לנחש — שם השדה הוא ההגדרה.
| הסקיל | מה נבנה בפועל |
|---|---|
myb-p-create-entity |
מודול שלם: טבלאות ← הרשאות ← נתונים ← כרטיס ← רשימה ← תפריט |
myb-p-multi-select-field |
שדה Array שנרנדר כ‑multi-pick של Select2 |
myb-p-parent-child-fields |
דרופ‑דאון בן שמסונן לפי בחירת האב |
myb-p-timestamp-field |
שדה Date + טריגר שממלא אותו |
myb-p-create-entity
Section titled “myb-p-create-entity”הסקיל הכבד בחבילה. בקשה אחת — “תקים לי מודול ניהול ספקים” — הופכת לחבילה שלמה:
מודול חדש├── שכבת נתונים│ ├── טבלה ראשית (Suppliers)│ ├── טבלת סטטוסים (SupplierStatuses) עם Name + Color│ ├── טבלאות בנות (SupplierOrders) עם Pointer חזרה│ └── CLP מבוסס-תפקידים על כל טבלה חדשה├── שכבת ממשק│ ├── דף כרטיס (apps/mybusiness/Supplier — יחיד)│ ├── דף רשימה (apps/mybusiness/Suppliers — רבים)│ └── פריט תפריט עם אייקון├── שכבת התנהגות│ └── קובץ JS חיצוני שמוזרם ל-S3 ומחובר לדף└── נתוני דמה └── סטטוסים עם צבעים + רשומות לדוגמההכלים לפי שלב
Section titled “הכלים לפי שלב”| השלב | הכלים |
|---|---|
| טבלאות | Create-Table · Set-Table-Permissions |
| נתונים | Create-Many (עם __type: "Pointer") · Create-Data |
| דף כרטיס | Create-Form-Page · Edit-Page · Get-Page-Content |
| דף רשימה | Create-Table-View-Page · Edit-Table-View · Edit-Page-CSS-JS |
| JS ותפריט | Upload-Public-File · Set-Page-Settings · Get-Page-Settings |
| שחזור | Get-Page-Versions · Set-Page-Version |
שדות הסטנדרט שכל ישות מקבלת
Section titled “שדות הסטנדרט שכל ישות מקבלת”| השדה | הטיפוס | הערה |
|---|---|---|
Name |
String | נוצר אוטומטית על ידי Create-Table |
Email / Phone |
String | |
StatusId |
Pointer → <Entity>Statuses |
נדרש לצביעה לפי סטטוס |
OwnerId |
Pointer → _User |
האחראי |
AccountId |
Pointer → Accounts |
רק אם הישות מתחברת ללקוחות |
Active |
Boolean | |
Comment |
String | שדה שנוסף ב‑Edit-Page נרנדר כ‑<input type="text"> (לא textarea) — ראו דפים וממשק |
ערכת הסטטוסים הדיפולטיבית: פעיל #28a745 · ממתין לאישור #ffc107 · מושהה #dc3545 · לא פעיל #6c757d.
שתי מוסכמות שקובעות אם זה יעבוד
Section titled “שתי מוסכמות שקובעות אם זה יעבוד”1. הרשאות מבוססות‑תפקידים, לא *.
CLP עם {"*": true} עובר את שכבת ה‑API (מאומת: קריאה אנונימית ב‑REST מצליחה) — אבל ה‑front-end של ה‑CRM בודק חברות בתפקיד, והמשתמש רואה “אין הרשאות”. טבלה שנוצרת ב‑Create-Table נולדת עם CLP ברירת מחדל שאינו מבוסס תפקידי CRM: find/get ל‑requiresAuthentication, ו‑create/update/delete ל‑role:Admin בלבד (CLP ריק לגמרי נוצר רק ב‑POST /parse/schemas גולמי). לכן Set-Table-Permissions היא קריאה חובה על כל טבלה חדשה, כדי שצוותי CRM/Sales/Support יוכלו לכתוב:
{ "find": { "role:CRM": true, "role:Admin": true, "role:Support": true }, "get": { "role:CRM": true, "role:Admin": true, "role:Support": true }, "create": { "role:CRM": true, "role:Admin": true, "role:Support": true }, "update": { "role:CRM": true, "role:Admin": true, "role:Support": true }, "delete": { "role:CRM": true, "role:Admin": true, "role:Support": true }, "addField": {}}הסכימה של Set-Table-Permissions דורשת את כל שש המפתחות — create, delete, update, find, get, addField. השמטת מפתח פעולה נדחית בוולידציה (addField: expected object). הדריסה השקטה היא בתוך פעולה: תפקיד שלא נכלל ב‑find שנשלח — מאבד את ההרשאה בלי שגיאה.
2. עמודת Pointer שמציגה מחרוזת היא type: "String".
זו הטעות שמונעת מהטבלה להיטען בכלל. ה‑type של עמודה הוא הטיפוס של הערך המוצג, לא של הקשר:
// ✓ נכון — מציג את שם הלקוח דרך Pointer{ "field": "AccountId.Name", "type": "String", "aggrField": "AccountId.Accounts.Name", "inlineOptions": { "type": "autocomplete", "required": false, "readonly": false }}
// ✗ שגוי — הטבלה לא תרנדר שורות{ "field": "AccountId.Name", "type": "Pointer" }השגיאה שתראו בקונסול: Cannot create property 'Name' on string.
לבחירת ה‑inlineOptions: autocomplete ל‑Pointer שמצביע לטבלה גדולה (Accounts, _User), ו‑select ל‑lookup קטן (סטטוסים, סוגים).
שלוש הדרכים לפתוח כרטיס — אל תערבבו
Section titled “שלוש הדרכים לפתוח כרטיס — אל תערבבו”| המנגנון | המסלול | מתי הוא חל |
|---|---|---|
| כרטיס MasterTicket | Mode=Ticket ← #SideModalTicket |
ישויות מובנות בלבד (Account / Task / Case). MCP אינו יכול ליצור כרטיסים על המאסטר הזה |
| כרטיס NewMaster | Mode=Modal ← #SideModal + src של #iframeModal |
כל כרטיס שנוצר ב‑Create-Form-Page — המסלול של הסקיל הזה |
| עריכה בשורה | editView: { openFrom: "inline" } |
עריכה מהירה בשורה, בלי כרטיס |
הקישור בין הרשימה לכרטיס נעשה ב‑Edit-Table-View:
"editView": { "openFrom": "modal-left", "useIframe": true, "page": "apps/mybusiness/Supplier", "popupWidthPercent": 45}Create-Form-Page מייצר תמיד דף על בסיס NewMaster, ואין כלי MCP או API לשנות masterPageId של דף קיים — הדפים יושבים באוסף פרטי של Simbla, לא במחלקות Parse. לכן הכרטיס עובד בתוך iframe אבל מציג שדות ריקים כשפותחים אותו ישירות לפי URL: NewMaster לא נופל חזרה לפרמטרים מה‑URL.
שגיאות שחוזרות, וההסבר שלהן
Section titled “שגיאות שחוזרות, וההסבר שלהן”| התסמין | הסיבה | התיקון |
|---|---|---|
| שום דבר ברשימה לא לחיץ, בלי שגיאה | הקישוט מכוון לעמודה שלא קיימת (ישות צומת) | שנו את {{PRIMARY_COL}} לעמודת התצוגה האמיתית |
| כפתור “חדש” פותח פאנל ריק | #iframeModal לא קיבל src — ה‑data-toggle המורש לבדו פותח מודאל ריק |
ה‑handler של התבנית: בונה URL ואז modal('show') |
| לחיצה ← “Page not found” | URL יחסי שהוכפל: apps/mybusiness/apps/mybusiness/… |
לגזור את הבסיס מ‑window.location.pathname, לא לבנות ידנית |
| הכרטיס נפתח ריק על רשומה קיימת | ב‑URL חסר פרמטר cls (יש oid בלבד). Mode=Ticket עצמו אינו האשם: עם oid וגם cls הרשומה נטענת כרגיל (mcp-c-053) |
לוודא ששני הפרמטרים בקישור. בכרטיס NewMaster לפתוח דרך Mode=Modal ← #SideModal/#iframeModal |
| לחיצה פותחת כרטיס של ישות אחרת | קובץ ה‑JS המורש (cases.js) עדיין מחובר |
להחליף בתבנית — להחליף, לא לתקן |
| הממשק אומר “אין הרשאות” | CLP עם * במקום תפקידים, או ACL חסר ברשומות |
Get-Table-Permissions ← CLP מבוסס‑תפקידים |
| כותרת הלשונית בדפדפן היא “Cases” | Create-Table-View-Page מעתיק גם את ה‑SEO title מהמקור |
Set-Page-Settings(pageId, title: "<שם הישות>") |
myb-p-multi-select-field
Section titled “myb-p-multi-select-field”שדה שמאחסן מערך של מזהי Pointer ונרנדר כ‑multi-pick של Select2 — בלי שורת JavaScript. הפלטפורמה מזהה את זה משם השדה:
| המקטע | המשמעות |
|---|---|
array_ |
קידומת באותיות קטנות ← לרנדר כ‑<select multiple> |
<purpose> |
snake_case חופשי (tags, sector, interests). לקריאוּת אנושית — הריצה מתעלמת ממנו |
_Pointer_ |
מפריד מילולי, P גדולה. איברי המערך הם Pointers |
<TargetTable> |
שם המחלקה של טבלת ה‑lookup, תלוי רישיות. ממנו נגזר ה‑targetclass על ה‑<select> |
הצעדים
Section titled “הצעדים”1. Get-Schema(className: "<HostTable>") — הטבלה קיימת? אין התנגשות שם?2. Create-Table + Create-Many — טבלת ה-lookup, עם 2-3 ערכים לפחות3. Add-Field-to-Table — השדה עצמו4. הצבה על דף הטופס — כאן יש פער, ראו למטה5. אימות — Get-Data על הרשומהAdd-Field-to-Table({ "table": "Accounts", "fields": [{ "name": "array_sector_Pointer_AccountTypes", "type": "Array", "label": "סקטור בחירה מרובה" }]})הטיפוס חייב להיות Array — לא Pointer ולא String.
הצבה על הדף — ב‑MCP, עם targetClass:
Edit-Page({ "pageId": "<formPageId>", "actions": [ { "actionType": "add-new-field", "fieldName": "array_sector_Pointer_AccountTypes", "label": "סקטור בחירה מרובה", "fieldType": "Array", "targetClass": "AccountTypes", "toExistingRow": "P272", "toExistingColumn": 0 }]})מה יוצא בסוף (מאומת בדפדפן):
<select class="select-pointer form-control" multiple id="array_sector_Pointer_AccountTypes" name="array_sector_Pointer_AccountTypes" targetclass="AccountTypes" data-subclass-pointers="[…]"></select>בזמן ריצה ה‑<select> מאותחל כ‑Select2 מרובה‑בחירה. התווית יושבת ב‑<label> שלצד השדה; ה‑<select> עצמו אינו נושא data-dictionary.
סינון בשאילתות
Section titled “סינון בשאילתות”תמיד containedIn, אף פעם לא equalTo:
{ "F": "array_tags_Pointer_Tags", "C": "containedIn", "V": ["zY2p6jpXqm", "io3Z6YsMcG"], "T": "Array" }להצגת השמות המחוברים בעמודת דוח — נקודות: array_tags_Pointer_Tags.Tags.Name.
מלכודות
Section titled “מלכודות”- קידומת או מפריד שגויים —
Array_AccountTypes,array_sector_AccountTypes(חסר_Pointer_),array_sector_pointer_AccountTypes(p קטנה) — כולם נכשלים. ה‑Pointerתלוי רישיות. - רישיות של טבלת היעד —
array_x_Pointer_accounttypesלא ייפתר. type: "Pointer"במקום"Array"— השדה יאחסן ערך יחיד; ה‑multi-pick ייראה עובד, אבל רק הבחירה האחרונה תישמר.- טבלת lookup ריקה — הדרופ‑דאון יציג “אין אפשרויות”. תמיד לזרוע ערכים לפני הדגמה.
- שינוי שם של שדה מערך אחרי שנשמרו נתונים — הערכים הקיימים מתייתמים; Parse שומר אותם תחת השם הישן. להעביר נתונים לפני שינוי שם.
- מערכים מבוססי
_Dictionary(הסגנון הישן,Sector_By_V) הם מנגנון אחר לגמרי — לא מכוסה כאן.
myb-p-parent-child-fields
Section titled “myb-p-parent-child-fields”דרופ‑דאון שמציג רק את הערכים ששייכים לבחירה בשדה אחר על אותו טופס — המנגנון הילידי, בלי JavaScript ובלי חוקי טופס.
שלושה שמות, מנגנון אחד:
| איפה | השם |
|---|---|
| עורך הדפים של Simbla | Define Parent |
פרמטר MCP ב‑Edit-Page |
subclassDepend |
| התכונה ב‑HTML המרונדר | data-subclass-depend="<ParentTable>" |
מה עובד ב‑MCP ומה לא
Section titled “מה עובד ב‑MCP ומה לא”| השלב | ב‑MCP? | הכלי |
|---|---|---|
| יצירה/בדיקה של טבלת האב והבן | ✓ | Create-Table · Get-Schema · Get-Data |
| שדה Pointer על טבלת הבן ← טבלת האב | ✓ | Add-Field-to-Table |
| תיוג כל רשומת בן באב שלה | ✓ | Create-Many · Update-Data |
| שדה האב על דף הטופס | ✓ | Edit-Page (add-new-field, fieldType: "Pointer") |
| שדה הבן עם ה‑Define Parent מחובר | ✓ | Edit-Page עם subclassDepend |
| הגדרת Define Parent על שדה שכבר על הדף | ✗ | עוקף: remove-existing-field ואז add-new-field מחדש |
| קריאה מובנית של ה‑Define Parent הנוכחי | ✗ | עוקף: Get-Page-Content(minimal: false) וחיפוש data-subclass-depend ב‑HTML |
המתכון המלא
Section titled “המתכון המלא”// 1. טבלת הבן, עם Pointer חזרה לאבCreate-Table({ "name": "SalesSubDepartments", "fields": [{ "name": "SalesDepartmentId", "type": "Pointer", "targetClass": "SalesDepartments", "label": "מחלקה" }]})
// 2. תיוג כל רשומת בן — Pointer יחיד, לא ArrayCreate-Many({ "table": "SalesSubDepartments", "data": [ { "Name": "B2B-Enterprise", "SalesDepartmentId": { "__type": "Pointer", "className": "SalesDepartments", "objectId": "L07ZV34Mqu" } }]})
// 3. שני שדות על טבלת המארח (הטופס)Add-Field-to-Table({ "table": "Sales", "fields": [ { "name": "DepartmentId", "type": "Pointer", "targetClass": "SalesDepartments", "label": "מחלקה" }, { "name": "SubDepartmentId", "type": "Pointer", "targetClass": "SalesSubDepartments", "label": "תת-מחלקה" }]})
// 4. האב על הדף — Pointer רגילEdit-Page({ "pageId": "<formPageId>", "actions": [ { "actionType": "add-new-field", "fieldName": "DepartmentId", "fieldType": "Pointer", "targetClass": "SalesDepartments", "toExistingRow": "P272", "toExistingColumn": 0 }]})
// 5. הבן על הדף — עם המילה הקסומהEdit-Page({ "pageId": "<formPageId>", "actions": [ { "actionType": "add-new-field", "fieldName": "SubDepartmentId", "fieldType": "Pointer", "targetClass": "SalesSubDepartments", "subclassDepend": "SalesDepartments", "toExistingRow": "P272", "toExistingColumn": 1 }]})זהו. רענון הטופס — והדרופ‑דאון הבן מונע על ידי בחירת האב.
מלכודות
Section titled “מלכודות”Arrayשל Pointers לאב — הסינון עובד בסמנטיקת שוויון על Pointer יחיד. חפיפה (ערך בן שמופיע תחת כמה אבות) אינה נתמכת: משכפלים את השורה, אחת לכל אב.- שכחתם
subclassDependב‑add-new-field— הקריאה מחזירהsuccessבשני המקרים. ההבדל מתגלה רק בזמן ריצה, כשהבן מתעלם מהאב. - בן לא מתויג (ה‑Pointer לאב ריק) — לא יופיע תחת שום אב.
- שני שדות Pointer על הטופס לאותה טבלת אב — הפלטפורמה לא יודעת מי מהם מניע את הסינון.
- ניסיון לסנן עם חוק טופס — חוקי טופס לא מסננים אפשרויות של Pointer. הפעולות היחידות הן
readonly,required,hidden,fixed-value,dynamic-value,formula-value,show-message,value-from-url— ואף אחת מהן אינה סינון. ראו אוטומציה.
שתי דרכים, שתיהן ב‑MCP:
// א. שכבת הנתונים — הסינון מוגדר נכון?Get-Data({ "table": "SalesSubDepartments", "keys": ["Name", "SalesDepartmentId"], "where": { "SalesDepartmentId": { "__type": "Pointer", "className": "SalesDepartments", "objectId": "<parentId>" } }})// ב. ה-HTML — הקישור על הדף?Get-Page-Content(pageId, minimal: false) → חפשו data-subclass-depend="SalesDepartments"myb-p-timestamp-field
Section titled “myb-p-timestamp-field”שדה Date שמתעד אוטומטית מתי שדה אחר נקבע או השתנה. שלושה כלים, ארבע דקות.
// 1. השדהAdd-Field-to-Table({ "table": "Sales", "fields": [{ "name": "OwnerIdSetDate", "type": "Date", "label": "תאריך שיוך אחראי" }] })
// 2. הטריגר — מתיSet-Trigger({ "tableName": "Sales", "type": "data change", "active": true, "name": "תיעוד תאריך שיוך אחראי", "events": ["create", "update"], "oneachupdate": true, "onSetFields": ["OwnerId"]})
// 3. הפעולה — מהSet-Trigger-Action({ "tableName": "Sales", "triggerId": "<id>", "triggerType": "data change", "actionType": "update-object", "actionData": { "update-object": { "targetClass": "Sales", "connection": "current.objectId", "fieldsValue": [{ "field": "OwnerIdSetDate", "type": "dynamic", "value": "updatedAt", "visibleVal": "updatedAt" }] }}})| ההחלטה | הפרמטר |
|---|---|
| כל שינוי, או רק בפעם הראשונה? | oneachupdate: true / false |
| גם ביצירה עם ערך? | events: ["create", "update"] |
| איזה שדה מנוטר? | onSetFields: ["OwnerId"] |
שדות Date מקבלים רק ערכים מסוג dynamic; updatedAt נותן את הרגע הנוכחי.
וריאציות
Section titled “וריאציות”חותמת מותנית — רק כשהסטטוס הופך ל“הושלם”:
"criterias": [{ "F": "SaleStatusId", "C": "equalTo", "V": "<objectId>", "T": "Pointer", "P": { "targetClass": "SaleStatuses", "visibleVal": "הושלם" }}]חותמת על רשומה קשורה — "connection": "source.AccountId" מעדכן את הלקוח המקושר במקום את הרשומה הנוכחית.
כמה שדות — טריגר נפרד לכל שדה. טריגר יחיד עם כמה onSetFields יורה על כל אחד מהם אבל כותב לשדה אחד, ולכן הוא עובד רק כ“תאריך שינוי אחרון” גנרי.
טריגרים כן יורים גם מכתיבות API ו‑Master Key (מאומת חי), ולכן בדיקה דרך Update-Data היא בדיקה תקפה:
Update-Data → שינוי השדה המנוטרGet-Data(table, objectId, keys: ["OwnerIdSetDate", "updatedAt"])ריק? בדקו את היומן: Get-Data(table: "_syslogTriggers", order: "-createdAt", limit: 3, keys: ["triggerId", "actionId", "error", "event"]) — שורה לכל פעולה, עם error (אין שדה triggerName; where ב‑MCP דורש מפתח fieldName — mcp-data-01).
- דפים וממשק — מה שקורה אחרי שהנתונים קיימים
- אוטומציה — הטריגרים שעליהם נשען
myb-p-timestamp-field - כלי סכימה ב‑MCP —
Get-Schema,Create-Table,Add-Field-to-Table - מודל הנתונים ומוסכמות