user-context migration
This commit is contained in:
@@ -0,0 +1,200 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "agjvjaleo6zfqe3",
|
||||||
|
"key": "user-oz8sxsyg8hznqa6",
|
||||||
|
"displayName": "IT",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "oz8sxsyg8hznqa6",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u3julc5juv1c5pq",
|
||||||
|
"key": "user-2lmwfhtdwuexmiv",
|
||||||
|
"displayName": "Ezekiel Ewing",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "2lmwfhtdwuexmiv",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "73z3dp9b6alhbl4",
|
||||||
|
"key": "user-1hbif9dbqng86h9",
|
||||||
|
"displayName": "Alfonso Ewing",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "1hbif9dbqng86h9",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "p65sesgbxah4chu",
|
||||||
|
"key": "user-fdve3i2gop4huym",
|
||||||
|
"displayName": "Heidi Mahan",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "fdve3i2gop4huym",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vo9x5s7x9jjq6zw",
|
||||||
|
"key": "user-ee4c6xmjrhypv4f",
|
||||||
|
"displayName": "Anita Pence",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": null,
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ez800j2vp4tp0tk",
|
||||||
|
"key": "user-h9uihmnkwngdu8y",
|
||||||
|
"displayName": "Beth Cardoza",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "h9uihmnkwngdu8y",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "u9517tx1x3hxrs5",
|
||||||
|
"key": "user-e9vck6v3pa951wn",
|
||||||
|
"displayName": "Rick Cardoza",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "e9vck6v3pa951wn",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2nlrw1wumhtbc98",
|
||||||
|
"key": "user-c1uzqhpacn65qt1",
|
||||||
|
"displayName": "Steve Brewer",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": null,
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2ttnn1vcdnniqci",
|
||||||
|
"key": "user-rrjoxd2wewmlrwy",
|
||||||
|
"displayName": "David Cardoza",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "rrjoxd2wewmlrwy",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a3nd0f8tpipjc3e",
|
||||||
|
"key": "user-sivbh2pwzyew5lb",
|
||||||
|
"displayName": "Timothy Cardoza",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "sivbh2pwzyew5lb",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ucj2hrr4htezrzo",
|
||||||
|
"key": "user-gk3u91dx0r79fo2",
|
||||||
|
"displayName": "Employee",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "handoff",
|
||||||
|
"targetUserId": "gk3u91dx0r79fo2",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lazcnjz2tsdoi0t",
|
||||||
|
"key": "user-mchpsyvh0wrcxv6",
|
||||||
|
"displayName": "Cortney Abrams",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "mchpsyvh0wrcxv6",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "p66mi0tscgw3sri",
|
||||||
|
"key": "user-ewdxrba73q68kqj",
|
||||||
|
"displayName": "Joel Snyder",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "ewdxrba73q68kqj",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asgpvdgthuoyhgx",
|
||||||
|
"key": "user-s1gwhknxaww38i3",
|
||||||
|
"displayName": "David Rader",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "s1gwhknxaww38i3",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5jdlsa7ebt1jd3i",
|
||||||
|
"key": "user-nc23vrshrg0qtdi",
|
||||||
|
"displayName": "Ben Poland",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "nc23vrshrg0qtdi",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "r27bfvcvig2n90m",
|
||||||
|
"key": "user-naztc2sg4b4fgfc",
|
||||||
|
"displayName": "Mark Huber",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "naztc2sg4b4fgfc",
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "il3piz7xfklzw3o",
|
||||||
|
"key": "testing-bucket",
|
||||||
|
"displayName": "Testing Bucket",
|
||||||
|
"kind": "bucket",
|
||||||
|
"policy": "handoff",
|
||||||
|
"targetUserId": null,
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w9zx3zqsi5r8zht",
|
||||||
|
"key": "vehicle-maintenance",
|
||||||
|
"displayName": "Vehicle Maintenance",
|
||||||
|
"kind": "bucket",
|
||||||
|
"policy": "handoff",
|
||||||
|
"targetUserId": null,
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "o6n72jlh3s6c42e",
|
||||||
|
"key": "shop",
|
||||||
|
"displayName": "Shop",
|
||||||
|
"kind": "bucket",
|
||||||
|
"policy": "handoff",
|
||||||
|
"targetUserId": null,
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cu4b1epkpwf15zs",
|
||||||
|
"key": "equipment-maintenance",
|
||||||
|
"displayName": "Equipment Maintenance",
|
||||||
|
"kind": "bucket",
|
||||||
|
"policy": "handoff",
|
||||||
|
"targetUserId": null,
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zdqmiqyfjx3z68d",
|
||||||
|
"key": "development",
|
||||||
|
"displayName": "Development",
|
||||||
|
"kind": "bucket",
|
||||||
|
"policy": "handoff",
|
||||||
|
"targetUserId": null,
|
||||||
|
"deletedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pjwskg0eo91n5y6",
|
||||||
|
"key": "user-i73wk8r7f2evbew",
|
||||||
|
"displayName": "Eddie",
|
||||||
|
"kind": "user",
|
||||||
|
"policy": "collaborative",
|
||||||
|
"targetUserId": "i73wk8r7f2evbew",
|
||||||
|
"deletedAt": null
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,82 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"email": "it@cardoza.construction",
|
||||||
|
"id": "oz8sxsyg8hznqa6",
|
||||||
|
"name": "IT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "pbserviceupdate@cardoza.construction",
|
||||||
|
"id": "x6hu9mgawxzfvk6",
|
||||||
|
"name": "PBServiceWorker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "eewing@cardoza.construction",
|
||||||
|
"id": "2lmwfhtdwuexmiv",
|
||||||
|
"name": "Ezekiel Ewing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "aewing@cardoza.construction",
|
||||||
|
"id": "1hbif9dbqng86h9",
|
||||||
|
"name": "Alfonso Ewing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "hmahan@cardoza.construction",
|
||||||
|
"id": "fdve3i2gop4huym",
|
||||||
|
"name": "Heidi Mahan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "bcardoza@cardoza.construction",
|
||||||
|
"id": "h9uihmnkwngdu8y",
|
||||||
|
"name": "Beth Cardoza"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "rcardoza@cardoza.construction",
|
||||||
|
"id": "e9vck6v3pa951wn",
|
||||||
|
"name": "Rick Cardoza"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "dcardoza@cardoza.construction",
|
||||||
|
"id": "rrjoxd2wewmlrwy",
|
||||||
|
"name": "David Cardoza"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "tcardoza@cardoza.construction",
|
||||||
|
"id": "sivbh2pwzyew5lb",
|
||||||
|
"name": "Timothy Cardoza"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "employee@cardoza.construction",
|
||||||
|
"id": "gk3u91dx0r79fo2",
|
||||||
|
"name": "Employee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "cabrams@cardoza.construction",
|
||||||
|
"id": "mchpsyvh0wrcxv6",
|
||||||
|
"name": "Cortney Abrams"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "jsnyder@cardoza.construction",
|
||||||
|
"id": "ewdxrba73q68kqj",
|
||||||
|
"name": "Joel Snyder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "drader@cardoza.construction",
|
||||||
|
"id": "s1gwhknxaww38i3",
|
||||||
|
"name": "David Rader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "mreds01@aol.com",
|
||||||
|
"id": "i73wk8r7f2evbew",
|
||||||
|
"name": "Eddie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "bpoland@cardoza.construction",
|
||||||
|
"id": "nc23vrshrg0qtdi",
|
||||||
|
"name": "Ben Poland"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "markhuber66@yahoo.com",
|
||||||
|
"id": "naztc2sg4b4fgfc",
|
||||||
|
"name": "Mark Huber"
|
||||||
|
}
|
||||||
|
]
|
||||||
+266
@@ -0,0 +1,266 @@
|
|||||||
|
{
|
||||||
|
"summary": {
|
||||||
|
"touched": 28,
|
||||||
|
"personalRefsAdded": 7,
|
||||||
|
"personalRefsRemoved": 0,
|
||||||
|
"ambiguousMultiPersonal": 44,
|
||||||
|
"failures": 0
|
||||||
|
},
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "z6s8kfpek0wowmp",
|
||||||
|
"title": "4150 Performing Arts - Material Lengths to Bill Mabary - see email",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb",
|
||||||
|
"nc23vrshrg0qtdi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hnn0ruj5kvcj7mi",
|
||||||
|
"title": "4150 Performing Arts - Acoustical",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb",
|
||||||
|
"nc23vrshrg0qtdi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ij2h8laauwxffyu",
|
||||||
|
"title": "4118 - Estes new wall CO Awaiting Conf and Sign",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb",
|
||||||
|
"nc23vrshrg0qtdi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "yz34jlzc29tq3ve",
|
||||||
|
"title": "4150 Performing Arts - Framing Shop Drawings",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb",
|
||||||
|
"nc23vrshrg0qtdi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9wu0icmgy8z0dpn",
|
||||||
|
"title": "4150 Acoustic CO pending response - Performing Arts Center",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb",
|
||||||
|
"nc23vrshrg0qtdi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lq7rzmxv7wtxdx0",
|
||||||
|
"title": "4118 Estes CO - Windows and Server Room Plywood",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5cs9kcfqkavgy5y",
|
||||||
|
"title": "Check in with Dad about further contact Kickapoo Ceilings",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb",
|
||||||
|
"h9uihmnkwngdu8y"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "59apl48j7se00cw",
|
||||||
|
"title": "reorganize metal & label",
|
||||||
|
"ownerIdBefore": "s1gwhknxaww38i3",
|
||||||
|
"ownerIdAfter": "s1gwhknxaww38i3",
|
||||||
|
"personalUserIds": [
|
||||||
|
"s1gwhknxaww38i3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5vn3zkn7wzaldsk",
|
||||||
|
"title": "test",
|
||||||
|
"ownerIdBefore": "gk3u91dx0r79fo2",
|
||||||
|
"ownerIdAfter": "gk3u91dx0r79fo2",
|
||||||
|
"personalUserIds": [
|
||||||
|
"gk3u91dx0r79fo2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "p2ut35nntf8s5on",
|
||||||
|
"title": "4841 - Andys HQ DD- Estimate",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kn1nxbmwgz7pyz9",
|
||||||
|
"title": "Equipment lists",
|
||||||
|
"ownerIdBefore": "nc23vrshrg0qtdi",
|
||||||
|
"ownerIdAfter": "nc23vrshrg0qtdi",
|
||||||
|
"personalUserIds": [
|
||||||
|
"nc23vrshrg0qtdi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "yqffg2agv88ggjo",
|
||||||
|
"title": "4118 - Estes - Lead Time Material Orders",
|
||||||
|
"ownerIdBefore": "nc23vrshrg0qtdi",
|
||||||
|
"ownerIdAfter": "nc23vrshrg0qtdi",
|
||||||
|
"personalUserIds": [
|
||||||
|
"nc23vrshrg0qtdi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n9tb1zd2qovqycz",
|
||||||
|
"title": "4850 - Intercounty Electric Rolla - Estimate",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5rsy930ttk14kdm",
|
||||||
|
"title": "4850 - Intercounty Electric Rolla - Estimate (Copy)",
|
||||||
|
"ownerIdBefore": "e9vck6v3pa951wn",
|
||||||
|
"ownerIdAfter": "e9vck6v3pa951wn",
|
||||||
|
"personalUserIds": [
|
||||||
|
"e9vck6v3pa951wn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zfmf0nil83svwan",
|
||||||
|
"title": "4150 Veregy - Joist lengths",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"nc23vrshrg0qtdi",
|
||||||
|
"sivbh2pwzyew5lb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "h2al3o03sfkn40z",
|
||||||
|
"title": "fix hilit gx-120",
|
||||||
|
"ownerIdBefore": null,
|
||||||
|
"ownerIdAfter": null,
|
||||||
|
"personalUserIds": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m3jk7vi6jhnai3y",
|
||||||
|
"title": "TEST Tim to Bucket",
|
||||||
|
"ownerIdBefore": null,
|
||||||
|
"ownerIdAfter": null,
|
||||||
|
"personalUserIds": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "aq1trlil8wnqgms",
|
||||||
|
"title": "buffer task",
|
||||||
|
"ownerIdBefore": "gk3u91dx0r79fo2",
|
||||||
|
"ownerIdAfter": "gk3u91dx0r79fo2",
|
||||||
|
"personalUserIds": [
|
||||||
|
"gk3u91dx0r79fo2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zbj1uyj0mvusqnb",
|
||||||
|
"title": "Door strike",
|
||||||
|
"ownerIdBefore": "2lmwfhtdwuexmiv",
|
||||||
|
"ownerIdAfter": "2lmwfhtdwuexmiv",
|
||||||
|
"personalUserIds": [
|
||||||
|
"2lmwfhtdwuexmiv"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9347spthvpgrpsn",
|
||||||
|
"title": "4845 - Pythian St Ceilings Colin - Estimate",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8xvb21a21k83gd1",
|
||||||
|
"title": "4150 Veregy Material Order",
|
||||||
|
"ownerIdBefore": "nc23vrshrg0qtdi",
|
||||||
|
"ownerIdAfter": "nc23vrshrg0qtdi",
|
||||||
|
"personalUserIds": [
|
||||||
|
"nc23vrshrg0qtdi",
|
||||||
|
"sivbh2pwzyew5lb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fzjbasn9r86merg",
|
||||||
|
"title": "test employee to tim handoff",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w1ce533o8voja8w",
|
||||||
|
"title": "Task to note via handoff",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wk754kcblrx1pxt",
|
||||||
|
"title": "Test task employee to note via handoff",
|
||||||
|
"ownerIdBefore": "gk3u91dx0r79fo2",
|
||||||
|
"ownerIdAfter": "gk3u91dx0r79fo2",
|
||||||
|
"personalUserIds": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "napobxne0ak37gn",
|
||||||
|
"title": "Test task employee to note via collab",
|
||||||
|
"ownerIdBefore": "gk3u91dx0r79fo2",
|
||||||
|
"ownerIdAfter": "gk3u91dx0r79fo2",
|
||||||
|
"personalUserIds": [
|
||||||
|
"gk3u91dx0r79fo2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1w3bbufjhoqq3sg",
|
||||||
|
"title": "Test task employee to tim via collab",
|
||||||
|
"ownerIdBefore": "gk3u91dx0r79fo2",
|
||||||
|
"ownerIdAfter": "gk3u91dx0r79fo2",
|
||||||
|
"personalUserIds": [
|
||||||
|
"gk3u91dx0r79fo2",
|
||||||
|
"sivbh2pwzyew5lb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6o855v9n1r3147h",
|
||||||
|
"title": "Test task employee to bucket via collab",
|
||||||
|
"ownerIdBefore": "gk3u91dx0r79fo2",
|
||||||
|
"ownerIdAfter": "gk3u91dx0r79fo2",
|
||||||
|
"personalUserIds": [
|
||||||
|
"gk3u91dx0r79fo2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "r9303qb1ejgfoaw",
|
||||||
|
"title": "4150 Veregy Redo Full Takeoff",
|
||||||
|
"ownerIdBefore": "sivbh2pwzyew5lb",
|
||||||
|
"ownerIdAfter": "sivbh2pwzyew5lb",
|
||||||
|
"personalUserIds": [
|
||||||
|
"sivbh2pwzyew5lb"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
"dev:devdata": "vite --host 0.0.0.0 --port 4001 --mode devdata",
|
"dev:devdata": "vite --host 0.0.0.0 --port 4001 --mode devdata",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"build:devdata": "tsc -b && vite build --mode devdata",
|
"build:devdata": "tsc -b && vite build --mode devdata",
|
||||||
|
"migration:context-ownership:backup": "bun run scripts/context-ownership-migration.ts backup",
|
||||||
|
"migration:context-ownership:report": "bun run scripts/context-ownership-migration.ts report",
|
||||||
|
"migration:context-ownership:migrate": "bun run scripts/context-ownership-migration.ts migrate",
|
||||||
"serve": "vite preview --host 0.0.0.0 --port 4000",
|
"serve": "vite preview --host 0.0.0.0 --port 4000",
|
||||||
"serve:devdata": "vite preview --host 0.0.0.0 --port 4002 --mode devdata",
|
"serve:devdata": "vite preview --host 0.0.0.0 --port 4002 --mode devdata",
|
||||||
"---PM2 COMMANDS---": "------------------",
|
"---PM2 COMMANDS---": "------------------",
|
||||||
|
|||||||
@@ -0,0 +1,596 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
type SharePolicy = "collaborative" | "handoff";
|
||||||
|
type ShareKind = "user" | "bucket" | "note";
|
||||||
|
|
||||||
|
type ShareContext = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
displayName: string;
|
||||||
|
kind: ShareKind;
|
||||||
|
policy: SharePolicy;
|
||||||
|
targetUserId?: string | null;
|
||||||
|
deletedAt?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NoteRecord = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
key: string;
|
||||||
|
tags: string[];
|
||||||
|
user: string;
|
||||||
|
tasks: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TaskShareRef = {
|
||||||
|
contextId: string;
|
||||||
|
key: string;
|
||||||
|
kind: ShareKind;
|
||||||
|
policy?: SharePolicy;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NoteRef = {
|
||||||
|
noteId: string;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ParsedTag =
|
||||||
|
| { kind: "share"; raw: string; key: string; display: string }
|
||||||
|
| { kind: "note"; raw: string; key: string; display: string }
|
||||||
|
| { kind: "label"; raw: string; key: string; display: string };
|
||||||
|
|
||||||
|
type TaskRecord = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
tags?: string[];
|
||||||
|
labelTags?: string[];
|
||||||
|
shareRefs?: TaskShareRef[];
|
||||||
|
noteRefs?: NoteRef[];
|
||||||
|
createdBy?: string | null;
|
||||||
|
user?: string | string[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Summary = {
|
||||||
|
touched: number;
|
||||||
|
personalRefsAdded: number;
|
||||||
|
personalRefsRemoved: number;
|
||||||
|
ambiguousMultiPersonal: number;
|
||||||
|
failures: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mode = (process.argv[2] || "report").toLowerCase();
|
||||||
|
if (!["backup", "report", "migrate"].includes(mode)) {
|
||||||
|
throw new Error("Usage: bun run scripts/context-ownership-migration.ts [backup|report|migrate]");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataMode = (process.env.TASGRID_DATA_MODE || "prod").toLowerCase() === "dev" ? "dev" : "prod";
|
||||||
|
const withDataSuffix = (base: string) => dataMode === "dev" ? `${base}_Dev` : base;
|
||||||
|
const TASGRID_COLLECTION = withDataSuffix("TasGrid");
|
||||||
|
const CONTEXTS_COLLECTION = withDataSuffix("TasGrid_Contexts");
|
||||||
|
const NOTES_COLLECTION = withDataSuffix("TasGrid_Notes");
|
||||||
|
const NOTE_HANDOFF_POLICY_TAG = "__note_policy_handoff__";
|
||||||
|
|
||||||
|
const POCKETBASE_URL = process.env.POCKETBASE_URL || "https://pocketbase.ccllc.pro";
|
||||||
|
let authToken = "";
|
||||||
|
|
||||||
|
const normalizeWhitespace = (value: string) => value.trim().replace(/\s+/g, " ");
|
||||||
|
const normalizeKey = (value: string) =>
|
||||||
|
normalizeWhitespace(value)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/^[@#]+/, "")
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "");
|
||||||
|
|
||||||
|
const buildShareTag = (displayName: string) => `@${normalizeWhitespace(displayName)}`;
|
||||||
|
const buildNoteTag = (value: string) => `#${normalizeWhitespace(value)}`;
|
||||||
|
const buildUserContextKey = (userId: string) => `user-${userId}`;
|
||||||
|
const unwrapRelationId = (value: unknown): string | null => {
|
||||||
|
if (typeof value === "string") return value || null;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const first = value[0];
|
||||||
|
return typeof first === "string" ? first : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTag = (rawTag: string): ParsedTag => {
|
||||||
|
const raw = normalizeWhitespace(rawTag);
|
||||||
|
const prefix = raw[0];
|
||||||
|
const display = prefix === "@" || prefix === "#" ? raw.slice(1).trim() : raw;
|
||||||
|
const key = normalizeKey(display);
|
||||||
|
|
||||||
|
if (prefix === "@") return { kind: "share", raw, key, display };
|
||||||
|
if (prefix === "#") return { kind: "note", raw, key, display };
|
||||||
|
return { kind: "label", raw, key, display };
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTags = (rawTags: string[] | undefined | null) =>
|
||||||
|
(rawTags || []).map(parseTag).filter(tag => !!tag.display);
|
||||||
|
|
||||||
|
const dedupeShareRefs = (refs: TaskShareRef[]) => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return refs.filter(ref => {
|
||||||
|
const key = `${ref.kind}:${ref.contextId}:${ref.key}`;
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const dedupeNoteRefs = (refs: NoteRef[]) => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return refs.filter(ref => {
|
||||||
|
const key = `${ref.noteId}:${ref.key}`;
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SHARE_POLICY_BY_KIND: Record<ShareKind, SharePolicy> = {
|
||||||
|
user: "collaborative",
|
||||||
|
bucket: "handoff",
|
||||||
|
note: "handoff"
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNoteSharePolicy = (note: Pick<NoteRecord, "tags"> | undefined): SharePolicy =>
|
||||||
|
note?.tags?.includes(NOTE_HANDOFF_POLICY_TAG) ? "handoff" : "collaborative";
|
||||||
|
|
||||||
|
const relationId = (record: any, field: string) => unwrapRelationId(record?.[field]);
|
||||||
|
|
||||||
|
const createOutputDir = async () => {
|
||||||
|
const outputDir = path.resolve(process.cwd(), "migration-output", `context-ownership-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
||||||
|
await fs.mkdir(outputDir, { recursive: true });
|
||||||
|
return outputDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryAuthEndpoint = async (pathname: string, email: string, password: string) => {
|
||||||
|
const response = await fetch(`${POCKETBASE_URL}${pathname}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ identity: email, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
ok: false as const,
|
||||||
|
status: response.status,
|
||||||
|
body: await response.text(),
|
||||||
|
pathname
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true as const,
|
||||||
|
payload: await response.json(),
|
||||||
|
pathname
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const auth = async () => {
|
||||||
|
const email = process.env.PB_ADMIN_EMAIL || process.env.PB_EMAIL;
|
||||||
|
const password = process.env.PB_ADMIN_PASSWORD || process.env.PB_PASSWORD;
|
||||||
|
if (!email || !password) {
|
||||||
|
throw new Error("Set PB_ADMIN_EMAIL/PB_ADMIN_PASSWORD (or PB_EMAIL/PB_PASSWORD) before running this script.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const authAttempts = [
|
||||||
|
await tryAuthEndpoint("/api/collections/_superusers/auth-with-password", email, password),
|
||||||
|
await tryAuthEndpoint("/api/admins/auth-with-password", email, password)
|
||||||
|
];
|
||||||
|
|
||||||
|
const success = authAttempts.find(attempt => attempt.ok);
|
||||||
|
if (!success) {
|
||||||
|
const details = authAttempts
|
||||||
|
.map(attempt => `${attempt.pathname}: ${attempt.status} ${attempt.body}`)
|
||||||
|
.join(" | ");
|
||||||
|
throw new Error(`PocketBase superuser/admin auth failed. ${details}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = success.payload;
|
||||||
|
authToken = payload.token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = async (pathname: string, init: RequestInit = {}) => {
|
||||||
|
const response = await fetch(`${POCKETBASE_URL}${pathname}`, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(authToken ? { Authorization: authToken } : {}),
|
||||||
|
...(init.headers || {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${response.status} ${response.statusText}: ${await response.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFullList = async (collection: string, query: Record<string, string> = {}) => {
|
||||||
|
const items: any[] = [];
|
||||||
|
let page = 1;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const search = new URLSearchParams({ perPage: "500", page: String(page), ...query });
|
||||||
|
const payload = await request(`/api/collections/${collection}/records?${search.toString()}`);
|
||||||
|
items.push(...(payload.items || []));
|
||||||
|
if (!payload.totalPages || page >= payload.totalPages) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecord = (collection: string, body: Record<string, unknown>) =>
|
||||||
|
request(`/api/collections/${collection}/records`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateRecord = (collection: string, id: string, body: Record<string, unknown>) =>
|
||||||
|
request(`/api/collections/${collection}/records/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncUserContexts = async (users: any[], contexts: ShareContext[]) => {
|
||||||
|
const nextContexts = [...contexts];
|
||||||
|
for (const user of users) {
|
||||||
|
const displayName = user.name || user.email;
|
||||||
|
const existing = nextContexts.find(context =>
|
||||||
|
context.kind === "user" &&
|
||||||
|
context.targetUserId === user.id &&
|
||||||
|
!context.deletedAt
|
||||||
|
);
|
||||||
|
if (!displayName || existing) continue;
|
||||||
|
|
||||||
|
const created = await createRecord(CONTEXTS_COLLECTION, {
|
||||||
|
key: buildUserContextKey(user.id),
|
||||||
|
displayName,
|
||||||
|
kind: "user",
|
||||||
|
policy: "collaborative",
|
||||||
|
targetUserId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
nextContexts.push({
|
||||||
|
id: created.id,
|
||||||
|
key: created.key,
|
||||||
|
displayName: created.displayName,
|
||||||
|
kind: created.kind,
|
||||||
|
policy: "collaborative",
|
||||||
|
targetUserId: relationId(created, "targetUserId"),
|
||||||
|
deletedAt: created.deletedAt || null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return nextContexts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNoteLinkMap = (notes: NoteRecord[]) => {
|
||||||
|
const links = new Map<string, NoteRef[]>();
|
||||||
|
for (const note of notes) {
|
||||||
|
for (const taskId of note.tasks || []) {
|
||||||
|
const existing = links.get(taskId) || [];
|
||||||
|
existing.push({ noteId: note.id, key: note.key });
|
||||||
|
links.set(taskId, existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return links;
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
await auth();
|
||||||
|
|
||||||
|
const [users, rawContexts, rawNotes, rawTasks] = await Promise.all([
|
||||||
|
getFullList("users", { fields: "id,name,email" }),
|
||||||
|
getFullList(CONTEXTS_COLLECTION),
|
||||||
|
getFullList(NOTES_COLLECTION),
|
||||||
|
getFullList(TASGRID_COLLECTION)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const contexts = mode === "migrate"
|
||||||
|
? await syncUserContexts(users, rawContexts.map((record: any) => ({
|
||||||
|
id: record.id,
|
||||||
|
key: record.key,
|
||||||
|
displayName: record.displayName,
|
||||||
|
kind: record.kind || "bucket",
|
||||||
|
policy: (record.policy || (record.kind === "user" ? "collaborative" : "handoff")) as SharePolicy,
|
||||||
|
targetUserId: relationId(record, "targetUserId"),
|
||||||
|
deletedAt: record.deletedAt || null
|
||||||
|
})))
|
||||||
|
: rawContexts.map((record: any) => ({
|
||||||
|
id: record.id,
|
||||||
|
key: record.key,
|
||||||
|
displayName: record.displayName,
|
||||||
|
kind: record.kind || "bucket",
|
||||||
|
policy: (record.policy || (record.kind === "user" ? "collaborative" : "handoff")) as SharePolicy,
|
||||||
|
targetUserId: relationId(record, "targetUserId"),
|
||||||
|
deletedAt: record.deletedAt || null
|
||||||
|
}));
|
||||||
|
|
||||||
|
const notes = rawNotes.map((record: any) => ({
|
||||||
|
id: record.id,
|
||||||
|
title: record.title,
|
||||||
|
key: record.key || normalizeKey(record.title || record.id),
|
||||||
|
tags: Array.isArray(record.tags) ? record.tags.filter(Boolean) : [],
|
||||||
|
user: relationId(record, "user") || "",
|
||||||
|
tasks: Array.isArray(record.tasks) ? record.tasks.filter(Boolean) : []
|
||||||
|
})) satisfies NoteRecord[];
|
||||||
|
|
||||||
|
const contextById = new Map(contexts.map(context => [context.id, context]));
|
||||||
|
const ownerContextByUserId = new Map(
|
||||||
|
contexts
|
||||||
|
.filter(context => context.kind === "user" && !!context.targetUserId && !context.deletedAt)
|
||||||
|
.map(context => [context.targetUserId as string, context] as const)
|
||||||
|
);
|
||||||
|
const noteById = new Map(notes.map(note => [note.id, note]));
|
||||||
|
const noteByKey = new Map(notes.map(note => [note.key, note]));
|
||||||
|
const noteLinksByTaskId = buildNoteLinkMap(notes);
|
||||||
|
|
||||||
|
const getContextByRef = (ref: TaskShareRef) => {
|
||||||
|
if (ref.kind === "note") {
|
||||||
|
const note = noteById.get(ref.contextId) || noteByKey.get(ref.key);
|
||||||
|
return note
|
||||||
|
? {
|
||||||
|
id: note.id,
|
||||||
|
key: note.key,
|
||||||
|
displayName: note.key || note.title,
|
||||||
|
kind: "note" as const,
|
||||||
|
policy: getNoteSharePolicy(note),
|
||||||
|
targetUserId: note.user
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contextById.get(ref.contextId)
|
||||||
|
|| contexts.find(context => !context.deletedAt && context.key === ref.key && context.kind === ref.kind)
|
||||||
|
|| contexts.find(context => !context.deletedAt && normalizeKey(context.displayName) === ref.key)
|
||||||
|
|| null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTaskSharePolicy = (ref: TaskShareRef): SharePolicy => {
|
||||||
|
if (ref.kind === "note") {
|
||||||
|
if (ref.policy) return ref.policy;
|
||||||
|
const note = noteById.get(ref.contextId) || noteByKey.get(ref.key);
|
||||||
|
return getNoteSharePolicy(note);
|
||||||
|
}
|
||||||
|
const context = getContextByRef(ref);
|
||||||
|
if (ref.policy) return ref.policy;
|
||||||
|
return context?.policy || DEFAULT_SHARE_POLICY_BY_KIND[ref.kind];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPersonalContextUserIdFromRef = (ref: TaskShareRef) => {
|
||||||
|
if (ref.kind !== "user") return null;
|
||||||
|
const context = getContextByRef(ref);
|
||||||
|
if (context?.targetUserId) return context.targetUserId;
|
||||||
|
if (ref.key.startsWith("user-")) return ref.key.replace(/^user-/, "") || null;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTaskPersonalRefs = (refs: TaskShareRef[]) =>
|
||||||
|
refs.filter(ref => ref.kind === "user" && !!getPersonalContextUserIdFromRef(ref));
|
||||||
|
|
||||||
|
const resolveMirroredOwnerId = (currentOwnerId: string | null, refs: TaskShareRef[], preferredOwnerId?: string | null) => {
|
||||||
|
const personalUserIds = [...new Set(
|
||||||
|
refs
|
||||||
|
.map(ref => getPersonalContextUserIdFromRef(ref))
|
||||||
|
.filter((value): value is string => !!value)
|
||||||
|
)];
|
||||||
|
|
||||||
|
if (personalUserIds.length === 1) return personalUserIds[0];
|
||||||
|
if (personalUserIds.length > 1 && preferredOwnerId && personalUserIds.includes(preferredOwnerId)) return preferredOwnerId;
|
||||||
|
return currentOwnerId || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTaskTags = (labelTags: string[], shareRefs: TaskShareRef[], noteRefs: NoteRef[], favoriteTag?: string) => {
|
||||||
|
const tags = [
|
||||||
|
...labelTags,
|
||||||
|
...shareRefs.map(ref => {
|
||||||
|
const context = getContextByRef(ref);
|
||||||
|
if (ref.kind === "note") return buildNoteTag(context?.key || ref.key);
|
||||||
|
if (context?.displayName) return buildShareTag(context.displayName);
|
||||||
|
if (ref.kind === "user" && ref.key.startsWith("user-")) return null;
|
||||||
|
return buildShareTag(ref.key);
|
||||||
|
}),
|
||||||
|
...noteRefs.map(ref => {
|
||||||
|
const note = noteById.get(ref.noteId) || noteByKey.get(ref.key);
|
||||||
|
return buildNoteTag(note?.key || ref.key);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
if (favoriteTag && !tags.includes(favoriteTag)) tags.push(favoriteTag);
|
||||||
|
return [...new Set(tags.filter((value): value is string => !!value))];
|
||||||
|
};
|
||||||
|
|
||||||
|
const summary: Summary = {
|
||||||
|
touched: 0,
|
||||||
|
personalRefsAdded: 0,
|
||||||
|
personalRefsRemoved: 0,
|
||||||
|
ambiguousMultiPersonal: 0,
|
||||||
|
failures: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const reportRows: any[] = [];
|
||||||
|
|
||||||
|
for (const record of rawTasks as TaskRecord[]) {
|
||||||
|
if ((record.tags || []).includes("__template__")) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const legacyTags = record.tags || [];
|
||||||
|
const parsedTags = parseTags(legacyTags).filter(tag => tag.raw !== "__template__");
|
||||||
|
const shareRefs = Array.isArray(record.shareRefs)
|
||||||
|
? record.shareRefs.map(ref => ({
|
||||||
|
contextId: ref.contextId,
|
||||||
|
key: ref.key,
|
||||||
|
kind: ref.kind,
|
||||||
|
policy: ref.policy
|
||||||
|
}))
|
||||||
|
: parsedTags
|
||||||
|
.filter((tag): tag is Extract<(typeof parsedTags)[number], { kind: "share" | "note" }> => tag.kind === "share" || tag.kind === "note")
|
||||||
|
.map(tag => {
|
||||||
|
if (tag.kind === "note") {
|
||||||
|
const note = noteByKey.get(tag.key);
|
||||||
|
return {
|
||||||
|
contextId: note?.id || tag.key,
|
||||||
|
key: tag.key,
|
||||||
|
kind: "note" as const
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = contexts.find(existing =>
|
||||||
|
!existing.deletedAt && (
|
||||||
|
existing.key === tag.key ||
|
||||||
|
normalizeKey(existing.displayName) === tag.key ||
|
||||||
|
buildShareTag(existing.displayName).toLowerCase() === tag.raw.toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
contextId: context?.id || tag.key,
|
||||||
|
key: tag.key,
|
||||||
|
kind: (context?.kind || "bucket") as "user" | "bucket",
|
||||||
|
policy: context?.policy
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const noteRefs = dedupeNoteRefs([
|
||||||
|
...(Array.isArray(record.noteRefs)
|
||||||
|
? record.noteRefs.map(ref => ({ noteId: ref.noteId, key: ref.key }))
|
||||||
|
: parsedTags
|
||||||
|
.filter(tag => tag.kind === "note")
|
||||||
|
.map(tag => {
|
||||||
|
const note = noteByKey.get(tag.key);
|
||||||
|
return {
|
||||||
|
noteId: note?.id || tag.key,
|
||||||
|
key: tag.key
|
||||||
|
};
|
||||||
|
})),
|
||||||
|
...(noteLinksByTaskId.get(record.id) || [])
|
||||||
|
]);
|
||||||
|
|
||||||
|
let nextShareRefs = dedupeShareRefs([
|
||||||
|
...shareRefs,
|
||||||
|
...noteRefs.map(ref => ({
|
||||||
|
contextId: ref.noteId,
|
||||||
|
key: ref.key,
|
||||||
|
kind: "note" as const,
|
||||||
|
policy: shareRefs.find(existing => existing.kind === "note" && (existing.contextId === ref.noteId || existing.key === ref.key))?.policy
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ownerId = unwrapRelationId(record.user);
|
||||||
|
const ownerContext = ownerId ? ownerContextByUserId.get(ownerId) : undefined;
|
||||||
|
const shouldRemoveOwnerPersonalRef = !!ownerId &&
|
||||||
|
getTaskPersonalRefs(nextShareRefs).length === 0 &&
|
||||||
|
nextShareRefs.some(ref => ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff");
|
||||||
|
|
||||||
|
if (ownerContext) {
|
||||||
|
const matchesOwnerContext = (ref: TaskShareRef) =>
|
||||||
|
ref.kind === "user" && (
|
||||||
|
ref.contextId === ownerContext.id ||
|
||||||
|
ref.key === ownerContext.key ||
|
||||||
|
normalizeKey(ownerContext.displayName) === ref.key
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldRemoveOwnerPersonalRef) {
|
||||||
|
const before = nextShareRefs.length;
|
||||||
|
nextShareRefs = nextShareRefs.filter(ref => !matchesOwnerContext(ref));
|
||||||
|
if (before !== nextShareRefs.length) {
|
||||||
|
summary.personalRefsRemoved += before - nextShareRefs.length;
|
||||||
|
}
|
||||||
|
} else if (!nextShareRefs.some(matchesOwnerContext)) {
|
||||||
|
nextShareRefs = [
|
||||||
|
{
|
||||||
|
contextId: ownerContext.id,
|
||||||
|
key: ownerContext.key,
|
||||||
|
kind: "user" as const,
|
||||||
|
policy: ownerContext.policy
|
||||||
|
},
|
||||||
|
...nextShareRefs
|
||||||
|
];
|
||||||
|
summary.personalRefsAdded += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextShareRefs = dedupeShareRefs(nextShareRefs);
|
||||||
|
|
||||||
|
const labelTags = Array.isArray(record.labelTags)
|
||||||
|
? record.labelTags
|
||||||
|
: parsedTags.filter(tag => tag.kind === "label").map(tag => tag.display);
|
||||||
|
const favoriteTag = legacyTags.find(tag => tag.endsWith("_favorite__"));
|
||||||
|
const nextTags = buildTaskTags(labelTags, nextShareRefs, noteRefs, favoriteTag);
|
||||||
|
const nextOwnerId = resolveMirroredOwnerId(ownerId, nextShareRefs, ownerId);
|
||||||
|
const personalUserIds = [...new Set(nextShareRefs.map(ref => getPersonalContextUserIdFromRef(ref)).filter((value): value is string => !!value))];
|
||||||
|
if (personalUserIds.length > 1) {
|
||||||
|
summary.ambiguousMultiPersonal += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changed =
|
||||||
|
JSON.stringify(nextShareRefs) !== JSON.stringify(record.shareRefs || []) ||
|
||||||
|
JSON.stringify(noteRefs) !== JSON.stringify(record.noteRefs || []) ||
|
||||||
|
JSON.stringify(nextTags) !== JSON.stringify(record.tags || []) ||
|
||||||
|
nextOwnerId !== ownerId;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
summary.touched += 1;
|
||||||
|
reportRows.push({
|
||||||
|
id: record.id,
|
||||||
|
title: record.title,
|
||||||
|
ownerIdBefore: ownerId,
|
||||||
|
ownerIdAfter: nextOwnerId,
|
||||||
|
personalUserIds
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mode === "migrate") {
|
||||||
|
await updateRecord(TASGRID_COLLECTION, record.id, {
|
||||||
|
user: nextOwnerId,
|
||||||
|
tags: nextTags,
|
||||||
|
labelTags,
|
||||||
|
shareRefs: nextShareRefs,
|
||||||
|
noteRefs
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
summary.failures += 1;
|
||||||
|
reportRows.push({
|
||||||
|
id: record.id,
|
||||||
|
title: record.title,
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "backup") {
|
||||||
|
const outputDir = await createOutputDir();
|
||||||
|
await Promise.all([
|
||||||
|
fs.writeFile(path.join(outputDir, "users.json"), JSON.stringify(users, null, 2)),
|
||||||
|
fs.writeFile(path.join(outputDir, "contexts.json"), JSON.stringify(contexts, null, 2)),
|
||||||
|
fs.writeFile(path.join(outputDir, "notes.json"), JSON.stringify(rawNotes, null, 2)),
|
||||||
|
fs.writeFile(path.join(outputDir, "tasks.json"), JSON.stringify(rawTasks, null, 2))
|
||||||
|
]);
|
||||||
|
console.log(JSON.stringify({ mode, dataMode, outputDir, counts: { users: users.length, contexts: contexts.length, notes: rawNotes.length, tasks: rawTasks.length } }, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "report") {
|
||||||
|
const outputDir = await createOutputDir();
|
||||||
|
await fs.writeFile(path.join(outputDir, "context-ownership-report.json"), JSON.stringify({ summary, tasks: reportRows }, null, 2));
|
||||||
|
console.log(JSON.stringify({ mode, dataMode, outputDir, summary }, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify({ mode, dataMode, summary }, null, 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+197
-105
@@ -217,8 +217,9 @@ export const startContextPolling = () => {
|
|||||||
try {
|
try {
|
||||||
// Fetch only recent updates (last 30s) to minimize payload
|
// Fetch only recent updates (last 30s) to minimize payload
|
||||||
const timeStr = new Date(Date.now() - 30000).toISOString();
|
const timeStr = new Date(Date.now() - 30000).toISOString();
|
||||||
|
const contextFilter = buildPersonalAccessFilter([personalOrUserId]);
|
||||||
const records = await pb.collection(TASGRID_COLLECTION).getFullList({
|
const records = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
filter: `${relationFilter("user", personalOrUserId)} && updated >= "${timeStr}"`,
|
filter: `${contextFilter} && updated >= "${timeStr}"`,
|
||||||
sort: '-updated',
|
sort: '-updated',
|
||||||
fields: getTaskListFields(),
|
fields: getTaskListFields(),
|
||||||
requestKey: 'context-poll'
|
requestKey: 'context-poll'
|
||||||
@@ -286,6 +287,7 @@ interface ScopedTaskgridPrefs {
|
|||||||
collapsedUntilUpdatedTasks?: Record<string, string>;
|
collapsedUntilUpdatedTasks?: Record<string, string>;
|
||||||
migratedStructuredTagsV2?: boolean;
|
migratedStructuredTagsV2?: boolean;
|
||||||
migratedPersonalContextRefsV1?: boolean;
|
migratedPersonalContextRefsV1?: boolean;
|
||||||
|
migratedContextOwnershipV2?: boolean;
|
||||||
lastCompletedTaskCleanupDate?: string; // YYYY-MM-DD, per user/environment
|
lastCompletedTaskCleanupDate?: string; // YYYY-MM-DD, per user/environment
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,14 +560,14 @@ export const getNoteSharePolicy = (note: Pick<Note, "tags"> | null | undefined):
|
|||||||
note?.tags?.includes(NOTE_HANDOFF_POLICY_TAG) ? "handoff" : "collaborative";
|
note?.tags?.includes(NOTE_HANDOFF_POLICY_TAG) ? "handoff" : "collaborative";
|
||||||
|
|
||||||
export const getTaskSharePolicy = (ref: Pick<TaskShareRef, "kind" | "key" | "policy" | "contextId">): "collaborative" | "handoff" => {
|
export const getTaskSharePolicy = (ref: Pick<TaskShareRef, "kind" | "key" | "policy" | "contextId">): "collaborative" | "handoff" => {
|
||||||
if (ref.policy) return ref.policy;
|
|
||||||
|
|
||||||
if (ref.kind === "note") {
|
if (ref.kind === "note") {
|
||||||
|
if (ref.policy) return ref.policy;
|
||||||
const note = store.notes.find(existing => existing.id === ref.contextId || existing.key === ref.key);
|
const note = store.notes.find(existing => existing.id === ref.contextId || existing.key === ref.key);
|
||||||
return getNoteSharePolicy(note);
|
return getNoteSharePolicy(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = getContextByRef(ref as TaskShareRef);
|
const context = getContextByRef(ref as TaskShareRef);
|
||||||
|
if (ref.policy) return ref.policy;
|
||||||
if (context?.policy) return context.policy;
|
if (context?.policy) return context.policy;
|
||||||
|
|
||||||
return DEFAULT_SHARE_POLICY_BY_KIND[ref.kind];
|
return DEFAULT_SHARE_POLICY_BY_KIND[ref.kind];
|
||||||
@@ -868,6 +870,106 @@ const getPersonalContext = (userId = pb.authStore.model?.id) =>
|
|||||||
) || null
|
) || null
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const getCurrentUserDisplayName = () =>
|
||||||
|
(pb.authStore.model?.name || pb.authStore.model?.email || "").trim() || null;
|
||||||
|
|
||||||
|
const getPersonalContextTag = (userId = pb.authStore.model?.id) => {
|
||||||
|
const context = getPersonalContext(userId);
|
||||||
|
if (context?.displayName) {
|
||||||
|
return buildShareTag(context.displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId && userId === pb.authStore.model?.id) {
|
||||||
|
const displayName = getCurrentUserDisplayName();
|
||||||
|
if (displayName) {
|
||||||
|
return buildShareTag(displayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapeFilterValue = (value: string) => value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||||
|
|
||||||
|
const buildPersonalAccessFilter = (userIds: string[], includeLegacyUser = true) => {
|
||||||
|
const clauses = userIds.flatMap(userId => {
|
||||||
|
const nextClauses: string[] = [];
|
||||||
|
const personalTag = getPersonalContextTag(userId);
|
||||||
|
if (personalTag) {
|
||||||
|
nextClauses.push(`tags ~ "${escapeFilterValue(personalTag)}"`);
|
||||||
|
}
|
||||||
|
if (includeLegacyUser) {
|
||||||
|
nextClauses.push(relationFilter("user", userId));
|
||||||
|
}
|
||||||
|
return nextClauses;
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
return clauses.length > 0 ? `(${clauses.join(" || ")})` : "false";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPersonalContextUserIdFromRef = (ref: TaskShareRef) => {
|
||||||
|
if (ref.kind !== "user") return null;
|
||||||
|
|
||||||
|
const context = getContextByRef(ref);
|
||||||
|
if (context?.targetUserId) {
|
||||||
|
return context.targetUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref.key.startsWith("user-")) {
|
||||||
|
return ref.key.replace(/^user-/, "") || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTaskPersonalRefs = (task: Pick<Task, "shareRefs">) =>
|
||||||
|
task.shareRefs.filter(ref => ref.kind === "user" && !!getPersonalContextUserIdFromRef(ref));
|
||||||
|
|
||||||
|
const getTaskPersonalUserIds = (task: Pick<Task, "shareRefs">) =>
|
||||||
|
[...new Set(getTaskPersonalRefs(task).map(ref => getPersonalContextUserIdFromRef(ref)).filter((value): value is string => !!value))];
|
||||||
|
|
||||||
|
const taskHasLegacyPersonalAccess = (task: Pick<Task, "ownerId" | "shareRefs">, userId: string) =>
|
||||||
|
task.ownerId === userId &&
|
||||||
|
getTaskPersonalRefs(task).length === 0 &&
|
||||||
|
!task.shareRefs.some(ref => ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff");
|
||||||
|
|
||||||
|
const taskHasPersonalContextAccess = (task: Pick<Task, "ownerId" | "shareRefs">, userId: string, allowLegacyFallback = true) =>
|
||||||
|
getTaskPersonalUserIds(task).includes(userId) ||
|
||||||
|
(allowLegacyFallback && taskHasLegacyPersonalAccess(task, userId));
|
||||||
|
|
||||||
|
const resolveMirroredOwnerId = (
|
||||||
|
currentOwnerId: string | null | undefined,
|
||||||
|
shareRefs: TaskShareRef[],
|
||||||
|
preferredOwnerId?: string | null
|
||||||
|
) => {
|
||||||
|
const personalUserIds = [...new Set(
|
||||||
|
shareRefs
|
||||||
|
.filter(ref => ref.kind === "user")
|
||||||
|
.map(ref => getPersonalContextUserIdFromRef(ref))
|
||||||
|
.filter((value): value is string => !!value)
|
||||||
|
)];
|
||||||
|
|
||||||
|
if (personalUserIds.length === 1) {
|
||||||
|
return personalUserIds[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (personalUserIds.length > 1 && preferredOwnerId && personalUserIds.includes(preferredOwnerId)) {
|
||||||
|
return preferredOwnerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentOwnerId || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const withMirroredOwnerId = (
|
||||||
|
currentOwnerId: string | null | undefined,
|
||||||
|
shareRefs: TaskShareRef[],
|
||||||
|
updates: Partial<Task>,
|
||||||
|
preferredOwnerId?: string | null
|
||||||
|
) => ({
|
||||||
|
...updates,
|
||||||
|
ownerId: resolveMirroredOwnerId(currentOwnerId, shareRefs, preferredOwnerId)
|
||||||
|
});
|
||||||
|
|
||||||
export const isCurrentUserContextTag = (tag: string) => {
|
export const isCurrentUserContextTag = (tag: string) => {
|
||||||
const normalizedTag = tag.trim().toLowerCase();
|
const normalizedTag = tag.trim().toLowerCase();
|
||||||
if (!normalizedTag.startsWith("@")) return false;
|
if (!normalizedTag.startsWith("@")) return false;
|
||||||
@@ -937,69 +1039,34 @@ export const matchesFilter = (task: Task) => {
|
|||||||
const activeShareContexts = task.shareRefs
|
const activeShareContexts = task.shareRefs
|
||||||
.map(ref => getContextByRef(ref))
|
.map(ref => getContextByRef(ref))
|
||||||
.filter((context): context is ShareContext => !!context && !context.deletedAt);
|
.filter((context): context is ShareContext => !!context && !context.deletedAt);
|
||||||
const collaborativeUserShare = task.shareRefs.some(ref => {
|
|
||||||
if (ref.kind !== "user" || getTaskSharePolicy(ref) !== "collaborative") return false;
|
|
||||||
const context = getContextByRef(ref);
|
|
||||||
return context?.targetUserId === currentUserId;
|
|
||||||
});
|
|
||||||
const bucketRefs = activeShareContexts.filter(context => context.kind === "bucket");
|
const bucketRefs = activeShareContexts.filter(context => context.kind === "bucket");
|
||||||
const hasHandoffContext = task.shareRefs.some(ref =>
|
|
||||||
ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff"
|
|
||||||
);
|
|
||||||
const inPinnedBucket = rawBucketRefs.some(ref =>
|
|
||||||
store.subscribedBuckets.includes(ref.contextId) ||
|
|
||||||
store.subscribedBuckets.some(bucketId => store.contexts.find(context => context.id === bucketId)?.key === ref.key)
|
|
||||||
);
|
|
||||||
const inAnyBucket = rawBucketRefs.length > 0;
|
|
||||||
const isPersonalView = ctx === "mine" || (
|
const isPersonalView = ctx === "mine" || (
|
||||||
typeof ctx === "object" &&
|
typeof ctx === "object" &&
|
||||||
"bucketId" in ctx &&
|
"bucketId" in ctx &&
|
||||||
!!personalContext &&
|
!!personalContext &&
|
||||||
ctx.bucketId === personalContext.id
|
ctx.bucketId === personalContext.id
|
||||||
);
|
);
|
||||||
const inPersonalContext = !!personalContext && task.shareRefs.some(ref =>
|
|
||||||
ref.kind === "user" &&
|
|
||||||
(
|
|
||||||
ref.contextId === personalContext.id ||
|
|
||||||
ref.key === personalContext.key ||
|
|
||||||
normalizeContextKey(personalContext.displayName) === ref.key
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 0. Context Filter
|
// 0. Context Filter
|
||||||
if (isPersonalView) {
|
if (isPersonalView) {
|
||||||
if (hasHandoffContext) return false;
|
if (!currentUserId || !taskHasPersonalContextAccess(task, currentUserId)) return false;
|
||||||
const isOwner = task.ownerId === currentUserId;
|
|
||||||
if (!isOwner && !collaborativeUserShare && !inPersonalContext) return false;
|
|
||||||
} else if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
} else if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||||
const activeBucketContext = store.contexts.find(context => context.id === (ctx as { bucketId: string }).bucketId);
|
const activeBucketContext = store.contexts.find(context => context.id === (ctx as { bucketId: string }).bucketId);
|
||||||
const activeBucketKey = activeBucketContext?.key || normalizeContextKey((ctx as { name: string }).name);
|
const activeBucketKey = activeBucketContext?.key || normalizeContextKey((ctx as { name: string }).name);
|
||||||
const inCurrentBucket = rawBucketRefs.some(ref =>
|
const inCurrentBucket = activeBucketContext?.kind === "user"
|
||||||
|
? !!activeBucketContext.targetUserId && taskHasPersonalContextAccess(task, activeBucketContext.targetUserId)
|
||||||
|
: rawBucketRefs.some(ref =>
|
||||||
ref.contextId === (ctx as { bucketId: string }).bucketId ||
|
ref.contextId === (ctx as { bucketId: string }).bucketId ||
|
||||||
ref.key === activeBucketKey
|
ref.key === activeBucketKey
|
||||||
) || bucketRefs.some(context => context.id === (ctx as { bucketId: string }).bucketId);
|
) || bucketRefs.some(context => context.id === (ctx as { bucketId: string }).bucketId);
|
||||||
if (!inCurrentBucket) return false;
|
if (!inCurrentBucket) return false;
|
||||||
} else {
|
} else {
|
||||||
const targetUserId = (ctx as { userId: string }).userId;
|
const targetUserId = (ctx as { userId: string }).userId;
|
||||||
const targetPersonalContext = getPersonalContext(targetUserId);
|
if (!taskHasPersonalContextAccess(task, targetUserId)) {
|
||||||
const inTargetPersonalContext = !!targetPersonalContext && task.shareRefs.some(ref =>
|
|
||||||
ref.kind === "user" &&
|
|
||||||
(
|
|
||||||
ref.contextId === targetPersonalContext.id ||
|
|
||||||
ref.key === targetPersonalContext.key ||
|
|
||||||
normalizeContextKey(targetPersonalContext.displayName) === ref.key
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (targetPersonalContext) {
|
|
||||||
if (!inTargetPersonalContext) return false;
|
|
||||||
} else if (task.ownerId !== targetUserId) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPersonalView && inAnyBucket && !inPinnedBucket && task.ownerId !== currentUserId) return false;
|
|
||||||
|
|
||||||
// Hide templates from all regular views
|
// Hide templates from all regular views
|
||||||
if (task.tags?.includes("__template__")) return false;
|
if (task.tags?.includes("__template__")) return false;
|
||||||
|
|
||||||
@@ -1046,7 +1113,7 @@ export const matchesFilter = (task: Task) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Owned By Me
|
// Owned By Me
|
||||||
if (f.ownedByMe && task.ownerId !== currentUserId) {
|
if (f.ownedByMe && (!currentUserId || !taskHasPersonalContextAccess(task, currentUserId))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1107,7 +1174,7 @@ createRoot(() => {
|
|||||||
// Calculate Top 20 Tasks
|
// Calculate Top 20 Tasks
|
||||||
const incompleteTasks = store.tasks.filter(t =>
|
const incompleteTasks = store.tasks.filter(t =>
|
||||||
!t.completed &&
|
!t.completed &&
|
||||||
t.ownerId === pb.authStore.model?.id &&
|
taskHasPersonalContextAccess(t, pb.authStore.model?.id || "", true) &&
|
||||||
!t.tags?.includes("__template__") &&
|
!t.tags?.includes("__template__") &&
|
||||||
!t.deletedAt
|
!t.deletedAt
|
||||||
);
|
);
|
||||||
@@ -1306,7 +1373,9 @@ const buildTaskMigrationPayload = (
|
|||||||
const ownerContext = task.ownerId ? ownerContextByUserId.get(task.ownerId) : undefined;
|
const ownerContext = task.ownerId ? ownerContextByUserId.get(task.ownerId) : undefined;
|
||||||
let nextShareRefs = dedupeShareRefs(task.shareRefs);
|
let nextShareRefs = dedupeShareRefs(task.shareRefs);
|
||||||
let nextNoteRefs = dedupeNoteRefs([...task.noteRefs, ...linkedNoteRefs]);
|
let nextNoteRefs = dedupeNoteRefs([...task.noteRefs, ...linkedNoteRefs]);
|
||||||
const hasHandoffContext = nextShareRefs.some(ref => ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff");
|
const shouldHideFromLegacyOwner = !!task.ownerId &&
|
||||||
|
getTaskPersonalRefs(task).length === 0 &&
|
||||||
|
nextShareRefs.some(ref => ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff");
|
||||||
|
|
||||||
if (ownerContext) {
|
if (ownerContext) {
|
||||||
const matchesOwnerContext = (ref: TaskShareRef) =>
|
const matchesOwnerContext = (ref: TaskShareRef) =>
|
||||||
@@ -1316,7 +1385,7 @@ const buildTaskMigrationPayload = (
|
|||||||
normalizeContextKey(ownerContext.displayName) === ref.key
|
normalizeContextKey(ownerContext.displayName) === ref.key
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasHandoffContext) {
|
if (shouldHideFromLegacyOwner) {
|
||||||
nextShareRefs = nextShareRefs.filter(ref => !matchesOwnerContext(ref));
|
nextShareRefs = nextShareRefs.filter(ref => !matchesOwnerContext(ref));
|
||||||
} else if (!nextShareRefs.some(matchesOwnerContext)) {
|
} else if (!nextShareRefs.some(matchesOwnerContext)) {
|
||||||
nextShareRefs = [
|
nextShareRefs = [
|
||||||
@@ -1332,12 +1401,18 @@ const buildTaskMigrationPayload = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextShareRefs = dedupeShareRefs(nextShareRefs);
|
nextShareRefs = dedupeShareRefs(nextShareRefs);
|
||||||
|
const mirroredOwnerId = resolveMirroredOwnerId(
|
||||||
|
task.ownerId || fallbackUserId || pb.authStore.model?.id || null,
|
||||||
|
nextShareRefs,
|
||||||
|
task.ownerId || fallbackUserId || null
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tags: buildTaskTags(task.labelTags, nextShareRefs, nextNoteRefs, favoriteTag),
|
tags: buildTaskTags(task.labelTags, nextShareRefs, nextNoteRefs, favoriteTag),
|
||||||
labelTags: task.labelTags,
|
labelTags: task.labelTags,
|
||||||
shareRefs: nextShareRefs,
|
shareRefs: nextShareRefs,
|
||||||
noteRefs: nextNoteRefs,
|
noteRefs: nextNoteRefs,
|
||||||
|
ownerId: mirroredOwnerId,
|
||||||
createdBy: task.createdBy || task.ownerId || fallbackUserId || pb.authStore.model?.id || ""
|
createdBy: task.createdBy || task.ownerId || fallbackUserId || pb.authStore.model?.id || ""
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1612,10 +1687,22 @@ export const loadNoteContent = async (id: string) => {
|
|||||||
// Helper to check if a task should be visible based on ownership, shares, or rules
|
// Helper to check if a task should be visible based on ownership, shares, or rules
|
||||||
const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
|
const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
|
||||||
const mappedTask = "shareRefs" in task ? task as Task : mapRecordToTask(task);
|
const mappedTask = "shareRefs" in task ? task as Task : mapRecordToTask(task);
|
||||||
if (mappedTask.ownerId === currentUserId) return true;
|
if (taskHasPersonalContextAccess(mappedTask, currentUserId)) return true;
|
||||||
if (store.supervisedContexts.some(context => context.ownerUserId === mappedTask.ownerId)) return true;
|
if (store.supervisedContexts.some(context => taskHasPersonalContextAccess(mappedTask, context.ownerUserId))) return true;
|
||||||
|
|
||||||
if (mappedTask.shareRefs.some(ref => ref.kind === "bucket")) return true;
|
const hasSubscribedBucket = mappedTask.shareRefs.some(ref =>
|
||||||
|
ref.kind === "bucket" && (
|
||||||
|
store.subscribedBuckets.includes(ref.contextId) ||
|
||||||
|
store.subscribedBuckets.some(bucketId => store.contexts.find(context => context.id === bucketId)?.key === ref.key)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (hasSubscribedBucket) return true;
|
||||||
|
|
||||||
|
const hasAccessibleNote = mappedTask.noteRefs.some(ref => {
|
||||||
|
const note = getNoteByRef(ref);
|
||||||
|
return !!note && (!note.isPrivate || note.user === currentUserId);
|
||||||
|
});
|
||||||
|
if (hasAccessibleNote) return true;
|
||||||
|
|
||||||
return mappedTask.shareRefs.some(ref => {
|
return mappedTask.shareRefs.some(ref => {
|
||||||
if (ref.kind !== "user" || getTaskSharePolicy(ref) !== "collaborative") return false;
|
if (ref.kind !== "user" || getTaskSharePolicy(ref) !== "collaborative") return false;
|
||||||
@@ -1879,7 +1966,7 @@ const syncUserContexts = async () => {
|
|||||||
key: buildUserContextKey(user.id),
|
key: buildUserContextKey(user.id),
|
||||||
displayName,
|
displayName,
|
||||||
kind: "user",
|
kind: "user",
|
||||||
policy: "handoff",
|
policy: "collaborative",
|
||||||
targetUserId: user.id
|
targetUserId: user.id
|
||||||
}, { requestKey: null }).catch(() => null);
|
}, { requestKey: null }).catch(() => null);
|
||||||
if (created) {
|
if (created) {
|
||||||
@@ -1933,7 +2020,7 @@ const syncSupervisedContexts = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const runPersonalContextMigration = async (force = false) => {
|
const runContextOwnershipMigration = async (force = false) => {
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = pb.authStore.model?.id;
|
||||||
if (!currentUserId) return;
|
if (!currentUserId) return;
|
||||||
|
|
||||||
@@ -1942,19 +2029,23 @@ const runPersonalContextMigration = async (force = false) => {
|
|||||||
const rawPrefs = userRec.Taskgrid_pref || {};
|
const rawPrefs = userRec.Taskgrid_pref || {};
|
||||||
const prefs = readScopedPrefs(rawPrefs);
|
const prefs = readScopedPrefs(rawPrefs);
|
||||||
|
|
||||||
if (!force && prefs.migratedPersonalContextRefsV1) {
|
if (!force && prefs.migratedContextOwnershipV2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contexts = store.contexts.length > 0
|
await syncUserContexts();
|
||||||
? store.contexts
|
const contexts = store.contexts.length > 0 ? store.contexts : await loadContextsIntoStore();
|
||||||
: await loadContextsIntoStore();
|
|
||||||
const ownerContextByUserId = new Map(
|
const ownerContextByUserId = new Map(
|
||||||
contexts
|
contexts
|
||||||
.filter(context => context.kind === "user" && !!context.targetUserId && !context.deletedAt)
|
.filter(context => context.kind === "user" && !!context.targetUserId && !context.deletedAt)
|
||||||
.map(context => [context.targetUserId as string, context])
|
.map(context => [context.targetUserId as string, context])
|
||||||
);
|
);
|
||||||
const noteLinksByTaskId = buildNoteLinkMap(store.notes);
|
const noteRecords = await pb.collection(NOTES_COLLECTION).getFullList({
|
||||||
|
fields: NOTE_LIST_FIELDS,
|
||||||
|
requestKey: null
|
||||||
|
}).catch(() => []);
|
||||||
|
const allNotes = noteRecords.map(record => mapRecordToNote(record));
|
||||||
|
const noteLinksByTaskId = buildNoteLinkMap(allNotes);
|
||||||
|
|
||||||
const taskRecords = await pb.collection(TASGRID_COLLECTION).getFullList({
|
const taskRecords = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
fields: getTaskListFields(),
|
fields: getTaskListFields(),
|
||||||
@@ -1976,19 +2067,27 @@ const runPersonalContextMigration = async (force = false) => {
|
|||||||
const sameShareRefs = JSON.stringify(payload.shareRefs) === JSON.stringify(task.shareRefs);
|
const sameShareRefs = JSON.stringify(payload.shareRefs) === JSON.stringify(task.shareRefs);
|
||||||
const sameNoteRefs = JSON.stringify(payload.noteRefs) === JSON.stringify(task.noteRefs);
|
const sameNoteRefs = JSON.stringify(payload.noteRefs) === JSON.stringify(task.noteRefs);
|
||||||
const sameTags = JSON.stringify(payload.tags) === JSON.stringify(task.tags);
|
const sameTags = JSON.stringify(payload.tags) === JSON.stringify(task.tags);
|
||||||
if (sameShareRefs && sameNoteRefs && sameTags && payload.createdBy === task.createdBy) continue;
|
const sameOwnerId = payload.ownerId === task.ownerId;
|
||||||
|
if (sameShareRefs && sameNoteRefs && sameTags && payload.createdBy === task.createdBy && sameOwnerId) continue;
|
||||||
|
|
||||||
await pb.collection(TASGRID_COLLECTION).update(task.id, payload, { requestKey: null });
|
const pbPayload = {
|
||||||
|
...payload,
|
||||||
|
user: payload.ownerId || task.ownerId || null
|
||||||
|
} as any;
|
||||||
|
delete pbPayload.ownerId;
|
||||||
|
|
||||||
|
await pb.collection(TASGRID_COLLECTION).update(task.id, pbPayload, { requestKey: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
await pb.collection('users').update(currentUserId, {
|
await pb.collection('users').update(currentUserId, {
|
||||||
Taskgrid_pref: writeScopedPrefs(rawPrefs, {
|
Taskgrid_pref: writeScopedPrefs(rawPrefs, {
|
||||||
...prefs,
|
...prefs,
|
||||||
migratedPersonalContextRefsV1: true
|
migratedPersonalContextRefsV1: true,
|
||||||
|
migratedContextOwnershipV2: true
|
||||||
})
|
})
|
||||||
}, { requestKey: null });
|
}, { requestKey: null });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[MIGRATION] Failed personal context backfill:", err);
|
console.error("[MIGRATION] Failed context ownership migration:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2001,7 +2100,7 @@ export const runDevDataMigration = async () => {
|
|||||||
|
|
||||||
await toast.promise((async () => {
|
await toast.promise((async () => {
|
||||||
await runLegacyTagMigration(true);
|
await runLegacyTagMigration(true);
|
||||||
await runPersonalContextMigration(true);
|
await runContextOwnershipMigration(true);
|
||||||
await loadContextsIntoStore();
|
await loadContextsIntoStore();
|
||||||
await syncSupervisedContexts();
|
await syncSupervisedContexts();
|
||||||
return "Dev data migration completed.";
|
return "Dev data migration completed.";
|
||||||
@@ -2027,7 +2126,7 @@ export const runProdDataMigration = async () => {
|
|||||||
|
|
||||||
await toast.promise((async () => {
|
await toast.promise((async () => {
|
||||||
await runLegacyTagMigration(true);
|
await runLegacyTagMigration(true);
|
||||||
await runPersonalContextMigration(true);
|
await runContextOwnershipMigration(true);
|
||||||
await loadContextsIntoStore();
|
await loadContextsIntoStore();
|
||||||
await syncSupervisedContexts();
|
await syncSupervisedContexts();
|
||||||
return "Production data migration completed.";
|
return "Production data migration completed.";
|
||||||
@@ -2065,8 +2164,9 @@ export const initStore = async () => {
|
|||||||
res.sort((a, b) => cachedQuickload.indexOf(a.id) - cachedQuickload.indexOf(b.id));
|
res.sort((a, b) => cachedQuickload.indexOf(a.id) - cachedQuickload.indexOf(b.id));
|
||||||
focusRecords = { items: res };
|
focusRecords = { items: res };
|
||||||
} else {
|
} else {
|
||||||
|
const personalAccessFilter = buildPersonalAccessFilter([currentUserId]);
|
||||||
focusRecords = await pb.collection(TASGRID_COLLECTION).getList(1, 30, {
|
focusRecords = await pb.collection(TASGRID_COLLECTION).getList(1, 30, {
|
||||||
filter: `${relationFilter("user", currentUserId)} && completed = false && deletedAt = ""`,
|
filter: `${personalAccessFilter} && completed = false && deletedAt = ""`,
|
||||||
sort: '-priority,dueDate',
|
sort: '-priority,dueDate',
|
||||||
requestKey: null
|
requestKey: null
|
||||||
});
|
});
|
||||||
@@ -2206,18 +2306,13 @@ export const initStore = async () => {
|
|||||||
const subscribedBucketTags = getSubscribedBucketContexts()
|
const subscribedBucketTags = getSubscribedBucketContexts()
|
||||||
.map(context => buildShareTag(context.displayName));
|
.map(context => buildShareTag(context.displayName));
|
||||||
const supervisedUserIds = store.supervisedContexts.map(context => context.ownerUserId);
|
const supervisedUserIds = store.supervisedContexts.map(context => context.ownerUserId);
|
||||||
const collaborativeKeys = store.contexts
|
const currentUserAccessFilter = buildPersonalAccessFilter(currentUserId ? [currentUserId] : []);
|
||||||
.filter(context =>
|
const supervisedAccessFilter = buildPersonalAccessFilter(supervisedUserIds);
|
||||||
!context.deletedAt &&
|
|
||||||
context.kind === "user" &&
|
|
||||||
context.targetUserId === currentUserId
|
|
||||||
)
|
|
||||||
.map(context => buildShareTag(context.displayName));
|
|
||||||
|
|
||||||
// 2a. Fetch ALL Incomplete Tasks (Metadata Only) to ensure availability
|
// 2a. Fetch ALL Incomplete Tasks (Metadata Only) to ensure availability
|
||||||
const incompletePromises = [
|
const incompletePromises = [
|
||||||
pb.collection(TASGRID_COLLECTION).getFullList({
|
pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
filter: `${relationFilter("user", currentUserId)} && completed = false`,
|
filter: `${currentUserAccessFilter} && completed = false`,
|
||||||
sort: '-created',
|
sort: '-created',
|
||||||
fields: getTaskListFields(),
|
fields: getTaskListFields(),
|
||||||
requestKey: null
|
requestKey: null
|
||||||
@@ -2228,14 +2323,8 @@ export const initStore = async () => {
|
|||||||
fields: getTaskListFields(),
|
fields: getTaskListFields(),
|
||||||
requestKey: null
|
requestKey: null
|
||||||
}).catch(() => []) : Promise.resolve([]),
|
}).catch(() => []) : Promise.resolve([]),
|
||||||
(collaborativeKeys.length > 0) ? pb.collection(TASGRID_COLLECTION).getFullList({
|
|
||||||
filter: `(${collaborativeKeys.map(tag => `tags ~ "${tag}"`).join(' || ')}) && completed = false`,
|
|
||||||
sort: '-created',
|
|
||||||
fields: getTaskListFields(),
|
|
||||||
requestKey: null
|
|
||||||
}).catch(() => []) : Promise.resolve([]),
|
|
||||||
(supervisedUserIds.length > 0) ? pb.collection(TASGRID_COLLECTION).getFullList({
|
(supervisedUserIds.length > 0) ? pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
filter: `(${supervisedUserIds.map(id => relationFilter("user", id)).join(' || ')}) && completed = false`,
|
filter: `${supervisedAccessFilter} && completed = false`,
|
||||||
sort: '-created',
|
sort: '-created',
|
||||||
fields: getTaskListFields(),
|
fields: getTaskListFields(),
|
||||||
requestKey: null
|
requestKey: null
|
||||||
@@ -2302,8 +2391,9 @@ export const initStore = async () => {
|
|||||||
|
|
||||||
if (remainingSlots > 0) {
|
if (remainingSlots > 0) {
|
||||||
try {
|
try {
|
||||||
|
const completedAccessFilter = buildPersonalAccessFilter(currentUserId ? [currentUserId] : []);
|
||||||
const recentCompleted = await pb.collection(TASGRID_COLLECTION).getList(1, remainingSlots, {
|
const recentCompleted = await pb.collection(TASGRID_COLLECTION).getList(1, remainingSlots, {
|
||||||
filter: `${relationFilter("user", currentUserId)} && completed = true`,
|
filter: `${completedAccessFilter} && completed = true`,
|
||||||
sort: '-updated',
|
sort: '-updated',
|
||||||
fields: getTaskListFields(),
|
fields: getTaskListFields(),
|
||||||
requestKey: null
|
requestKey: null
|
||||||
@@ -2346,7 +2436,7 @@ export const initStore = async () => {
|
|||||||
// Run one-time migration automatically only in dev data mode.
|
// Run one-time migration automatically only in dev data mode.
|
||||||
if (TASGRID_IS_DEV_DATA) {
|
if (TASGRID_IS_DEV_DATA) {
|
||||||
await runLegacyTagMigration();
|
await runLegacyTagMigration();
|
||||||
await runPersonalContextMigration();
|
await runContextOwnershipMigration();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate Templates load (less critical)
|
// Separate Templates load (less critical)
|
||||||
@@ -2622,7 +2712,13 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
|
|||||||
// Apply Handoff Logic (e.g. transfer ownership if tagging for another user)
|
// Apply Handoff Logic (e.g. transfer ownership if tagging for another user)
|
||||||
// We treat the entire new task as "updates" to check against itself
|
// We treat the entire new task as "updates" to check against itself
|
||||||
const handoffUpdates = checkForHandoffs(initialTask, initialTask);
|
const handoffUpdates = checkForHandoffs(initialTask, initialTask);
|
||||||
const newTask = { ...initialTask, ...handoffUpdates };
|
const mirroredUpdates = withMirroredOwnerId(
|
||||||
|
initialTask.ownerId,
|
||||||
|
handoffUpdates.shareRefs || initialTask.shareRefs,
|
||||||
|
handoffUpdates,
|
||||||
|
handoffUpdates.ownerId || initialTask.ownerId
|
||||||
|
);
|
||||||
|
const newTask = { ...initialTask, ...mirroredUpdates };
|
||||||
|
|
||||||
// Optimistic UI update
|
// Optimistic UI update
|
||||||
setStore("tasks", (t) => [newTask, ...t]);
|
setStore("tasks", (t) => [newTask, ...t]);
|
||||||
@@ -2710,7 +2806,7 @@ export const copyTask = async (id: string) => {
|
|||||||
|
|
||||||
const checkForHandoffs = (task: Task, updates: Partial<Task>): Partial<Task> => {
|
const checkForHandoffs = (task: Task, updates: Partial<Task>): Partial<Task> => {
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = pb.authStore.model?.id;
|
||||||
if (!currentUserId || task.ownerId !== currentUserId) return updates;
|
if (!currentUserId || !taskHasPersonalContextAccess(task, currentUserId)) return updates;
|
||||||
|
|
||||||
const refs = updates.shareRefs || task.shareRefs;
|
const refs = updates.shareRefs || task.shareRefs;
|
||||||
const senderPersonalContext = getPersonalContext(currentUserId);
|
const senderPersonalContext = getPersonalContext(currentUserId);
|
||||||
@@ -2724,6 +2820,12 @@ const checkForHandoffs = (task: Task, updates: Partial<Task>): Partial<Task> =>
|
|||||||
|
|
||||||
for (const ref of refs) {
|
for (const ref of refs) {
|
||||||
const context = getContextByRef(ref);
|
const context = getContextByRef(ref);
|
||||||
|
if (senderPersonalContext && ref.kind === "user" && (
|
||||||
|
ref.contextId === senderPersonalContext.id ||
|
||||||
|
ref.key === senderPersonalContext.key
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!context || getTaskSharePolicy(ref) !== "handoff") continue;
|
if (!context || getTaskSharePolicy(ref) !== "handoff") continue;
|
||||||
const nextShareRefs = stripSenderPersonalContext(refs);
|
const nextShareRefs = stripSenderPersonalContext(refs);
|
||||||
const labelTags = updates.labelTags || task.labelTags;
|
const labelTags = updates.labelTags || task.labelTags;
|
||||||
@@ -2801,6 +2903,13 @@ export const updateTask = async (id: string, updates: Partial<Task>) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
finalUpdates = checkForHandoffs(currentTask, finalUpdates);
|
finalUpdates = checkForHandoffs(currentTask, finalUpdates);
|
||||||
|
const mirroredShareRefs = finalUpdates.shareRefs || currentTask.shareRefs;
|
||||||
|
finalUpdates = withMirroredOwnerId(
|
||||||
|
finalUpdates.ownerId ?? currentTask.ownerId,
|
||||||
|
mirroredShareRefs,
|
||||||
|
finalUpdates,
|
||||||
|
finalUpdates.ownerId ?? currentTask.ownerId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimistic update with timestamp to prevent stale realtime events
|
// Optimistic update with timestamp to prevent stale realtime events
|
||||||
@@ -3141,16 +3250,11 @@ export const loadAllHistory = async () => {
|
|||||||
const bucketTags = getSubscribedBucketContexts()
|
const bucketTags = getSubscribedBucketContexts()
|
||||||
.map(context => buildShareTag(context.displayName));
|
.map(context => buildShareTag(context.displayName));
|
||||||
const supervisedUserIds = store.supervisedContexts.map(context => context.ownerUserId);
|
const supervisedUserIds = store.supervisedContexts.map(context => context.ownerUserId);
|
||||||
const collaborativeTags = store.contexts
|
const userAccessFilter = buildPersonalAccessFilter([userId]);
|
||||||
.filter(context =>
|
const supervisedAccessFilter = buildPersonalAccessFilter(supervisedUserIds);
|
||||||
!context.deletedAt &&
|
|
||||||
context.kind === "user" &&
|
|
||||||
context.targetUserId === userId
|
|
||||||
)
|
|
||||||
.map(context => buildShareTag(context.displayName));
|
|
||||||
|
|
||||||
const promises = [
|
const promises = [
|
||||||
pb.collection(TASGRID_COLLECTION).getFullList({ filter: relationFilter("user", userId), sort: '-created', fields: getTaskListFields(), requestKey: null })
|
pb.collection(TASGRID_COLLECTION).getFullList({ filter: userAccessFilter, sort: '-created', fields: getTaskListFields(), requestKey: null })
|
||||||
];
|
];
|
||||||
|
|
||||||
if (bucketTags.length > 0) {
|
if (bucketTags.length > 0) {
|
||||||
@@ -3162,18 +3266,9 @@ export const loadAllHistory = async () => {
|
|||||||
}).catch(() => []));
|
}).catch(() => []));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collaborativeTags.length > 0) {
|
|
||||||
promises.push(pb.collection(TASGRID_COLLECTION).getFullList({
|
|
||||||
filter: collaborativeTags.map(tag => `tags ~ "${tag}"`).join(' || '),
|
|
||||||
sort: '-created',
|
|
||||||
fields: getTaskListFields(),
|
|
||||||
requestKey: null
|
|
||||||
}).catch(() => []));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supervisedUserIds.length > 0) {
|
if (supervisedUserIds.length > 0) {
|
||||||
promises.push(pb.collection(TASGRID_COLLECTION).getFullList({
|
promises.push(pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
filter: supervisedUserIds.map(id => relationFilter("user", id)).join(' || '),
|
filter: supervisedAccessFilter,
|
||||||
sort: '-created',
|
sort: '-created',
|
||||||
fields: getTaskListFields(),
|
fields: getTaskListFields(),
|
||||||
requestKey: null
|
requestKey: null
|
||||||
@@ -3406,10 +3501,7 @@ export const loadTasksForOwner = async (userId: string) => {
|
|||||||
if (!pb.authStore.isValid || !userId) return;
|
if (!pb.authStore.isValid || !userId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const targetPersonalContext = getPersonalContext(userId);
|
const filter = buildPersonalAccessFilter([userId]);
|
||||||
const filter = targetPersonalContext
|
|
||||||
? `tags ~ "${buildShareTag(targetPersonalContext.displayName)}"`
|
|
||||||
: relationFilter("user", userId);
|
|
||||||
const records = await pb.collection(TASGRID_COLLECTION).getFullList({
|
const records = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
filter,
|
filter,
|
||||||
sort: '-created',
|
sort: '-created',
|
||||||
|
|||||||
Reference in New Issue
Block a user