forked from vikunja/frontend
Compare commits
2 Commits
e2e743708c
...
8d4aab4fc7
Author | SHA1 | Date |
---|---|---|
WofWca | 8d4aab4fc7 | |
WofWca | 080fc489d7 |
|
@ -16,6 +16,9 @@
|
|||
<strong>We're sorry but Vikunja doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- TODO_OFFLINE maybe need to dynamically import it instead.
|
||||
Not sure if it works ok inside Delta Chat. -->
|
||||
<script type="module" src="/webxdc.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script>
|
||||
//
|
||||
|
|
|
@ -91,6 +91,9 @@
|
|||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.6",
|
||||
"workbox-precaching": "6.5.4",
|
||||
"y-indexeddb": "^9.0.10",
|
||||
"y-webrtc": "^10.2.5",
|
||||
"yjs": "^13.5.51",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
102
pnpm-lock.yaml
102
pnpm-lock.yaml
|
@ -100,6 +100,9 @@ specifiers:
|
|||
wait-on: 7.0.1
|
||||
workbox-cli: 6.5.4
|
||||
workbox-precaching: 6.5.4
|
||||
y-indexeddb: ^9.0.10
|
||||
y-webrtc: ^10.2.5
|
||||
yjs: ^13.5.51
|
||||
zhyswan-vuedraggable: 4.1.3
|
||||
|
||||
dependencies:
|
||||
|
@ -147,6 +150,9 @@ dependencies:
|
|||
vue-i18n: 9.2.2_vue@3.2.47
|
||||
vue-router: 4.1.6_vue@3.2.47
|
||||
workbox-precaching: 6.5.4
|
||||
y-indexeddb: 9.0.10_yjs@13.5.51
|
||||
y-webrtc: 10.2.5
|
||||
yjs: 13.5.51
|
||||
zhyswan-vuedraggable: 4.1.3_vue@3.2.47
|
||||
|
||||
devDependencies:
|
||||
|
@ -5174,7 +5180,6 @@ packages:
|
|||
|
||||
/base64-js/1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
dev: true
|
||||
|
||||
/bcrypt-pbkdf/1.0.2:
|
||||
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
|
||||
|
@ -5417,7 +5422,6 @@ packages:
|
|||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
dev: true
|
||||
|
||||
/builtin-modules/3.2.0:
|
||||
resolution: {integrity: sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==}
|
||||
|
@ -7188,6 +7192,10 @@ packages:
|
|||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/err-code/3.0.1:
|
||||
resolution: {integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==}
|
||||
dev: false
|
||||
|
||||
/error-ex/1.3.2:
|
||||
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
||||
dependencies:
|
||||
|
@ -8338,6 +8346,10 @@ packages:
|
|||
node-source-walk: 5.0.0
|
||||
dev: true
|
||||
|
||||
/get-browser-rtc/1.1.0:
|
||||
resolution: {integrity: sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==}
|
||||
dev: false
|
||||
|
||||
/get-caller-file/2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
@ -9067,7 +9079,6 @@ packages:
|
|||
|
||||
/ieee754/1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
dev: true
|
||||
|
||||
/ignore/4.0.6:
|
||||
resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==}
|
||||
|
@ -9130,7 +9141,6 @@ packages:
|
|||
|
||||
/inherits/2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
dev: true
|
||||
|
||||
/ini/1.3.7:
|
||||
resolution: {integrity: sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==}
|
||||
|
@ -9676,6 +9686,10 @@ packages:
|
|||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/isomorphic.js/0.2.5:
|
||||
resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
|
||||
dev: false
|
||||
|
||||
/isstream/0.1.2:
|
||||
resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
|
||||
dev: true
|
||||
|
@ -10068,6 +10082,14 @@ packages:
|
|||
type-check: 0.4.0
|
||||
dev: true
|
||||
|
||||
/lib0/0.2.73:
|
||||
resolution: {integrity: sha512-aJJIElCLWnHMcYZPtsM07QoSfHwpxCy4VUzBYGXFYEmh/h2QS5uZNbCCfL0CqnkOE30b7Tp9DVfjXag+3qzZjQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
isomorphic.js: 0.2.5
|
||||
dev: false
|
||||
|
||||
/light-my-request/5.8.0:
|
||||
resolution: {integrity: sha512-4BtD5C+VmyTpzlDPCZbsatZMJVgUIciSOwYhJDCbLffPZ35KoDkDj4zubLeHDEb35b4kkPeEv5imbh+RJxK/Pg==}
|
||||
dependencies:
|
||||
|
@ -12626,7 +12648,6 @@ packages:
|
|||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/range-parser/1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
|
@ -12714,7 +12735,6 @@ packages:
|
|||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
dev: true
|
||||
|
||||
/readable-stream/4.3.0:
|
||||
resolution: {integrity: sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==}
|
||||
|
@ -13097,7 +13117,6 @@ packages:
|
|||
|
||||
/safe-buffer/5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
dev: true
|
||||
|
||||
/safe-json-stringify/1.2.0:
|
||||
resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==}
|
||||
|
@ -13300,6 +13319,20 @@ packages:
|
|||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||
dev: true
|
||||
|
||||
/simple-peer/9.11.1:
|
||||
resolution: {integrity: sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==}
|
||||
dependencies:
|
||||
buffer: 6.0.3
|
||||
debug: 4.3.4
|
||||
err-code: 3.0.1
|
||||
get-browser-rtc: 1.1.0
|
||||
queue-microtask: 1.2.3
|
||||
randombytes: 2.1.0
|
||||
readable-stream: 3.6.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/simple-swizzle/0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
dependencies:
|
||||
|
@ -13712,7 +13745,6 @@ packages:
|
|||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/stringify-object/3.3.0:
|
||||
resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==}
|
||||
|
@ -14647,7 +14679,6 @@ packages:
|
|||
|
||||
/util-deprecate/1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
dev: true
|
||||
|
||||
/utils-merge/1.0.1:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
|
@ -15416,6 +15447,21 @@ packages:
|
|||
signal-exit: 3.0.7
|
||||
dev: true
|
||||
|
||||
/ws/7.5.9:
|
||||
resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==}
|
||||
engines: {node: '>=8.3.0'}
|
||||
requiresBuild: true
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ^5.0.2
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/ws/8.11.0:
|
||||
resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
@ -15466,6 +15512,37 @@ packages:
|
|||
engines: {node: '>=0.4'}
|
||||
dev: true
|
||||
|
||||
/y-indexeddb/9.0.10_yjs@13.5.51:
|
||||
resolution: {integrity: sha512-Tz4IzLZ20Pe8LTjXU125k2+7caERsX4ANsrjskHMm3yXUX4v9kBga/kK7ctT05P0uAj+glQComkAglY1qey7zg==}
|
||||
peerDependencies:
|
||||
yjs: ^13.0.0
|
||||
dependencies:
|
||||
lib0: 0.2.73
|
||||
yjs: 13.5.51
|
||||
dev: false
|
||||
|
||||
/y-protocols/1.0.5:
|
||||
resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==}
|
||||
dependencies:
|
||||
lib0: 0.2.73
|
||||
dev: false
|
||||
|
||||
/y-webrtc/10.2.5:
|
||||
resolution: {integrity: sha512-ZyBNvTI5L28sQ2PQI0T/JvyWgvuTq05L21vGkIlcvNLNSJqAaLCBJRe3FHEqXoaogqWmRcEAKGfII4ErNXMnNw==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
lib0: 0.2.73
|
||||
simple-peer: 9.11.1
|
||||
y-protocols: 1.0.5
|
||||
optionalDependencies:
|
||||
ws: 7.5.9
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/y18n/5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -15529,6 +15606,13 @@ packages:
|
|||
fd-slicer: 1.1.0
|
||||
dev: true
|
||||
|
||||
/yjs/13.5.51:
|
||||
resolution: {integrity: sha512-F1Nb3z3TdandD80IAeQqgqy/2n9AhDLcXoBhZvCUX1dNVe0ef7fIwi6MjSYaGAYF2Ev8VcLcsGnmuGGOl7AWbw==}
|
||||
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
|
||||
dependencies:
|
||||
lib0: 0.2.73
|
||||
dev: false
|
||||
|
||||
/yn/3.1.1:
|
||||
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { IBucket } from '@/modelTypes/IBucket'
|
||||
import { ensureCreateSyncedYDoc } from './sync'
|
||||
import { getAllTasks, getTasksOfBucket } from './tasks'
|
||||
import { defaultPositionIfZero } from './utils/calculateDefaultPosition'
|
||||
|
||||
|
@ -59,15 +60,20 @@ function getInitialBuckets(): IBucketWithoutTasks[] {
|
|||
]
|
||||
}
|
||||
|
||||
function getAllBucketsWithoutTasks(): IBucketWithoutTasks[] {
|
||||
const fromStorage = localStorage.getItem('buckets')
|
||||
if (!fromStorage) {
|
||||
async function getAllBucketsWithoutTasks(): Promise<IBucketWithoutTasks[]> {
|
||||
const fromYdoc = (await ensureCreateSyncedYDoc())
|
||||
// Yes, a map with an only element. I'm too lazy to make it a Y.Array.
|
||||
.getMap('bucketsMap')
|
||||
.get('buckets')
|
||||
|
||||
// const fromStorage = localStorage.getItem('buckets')
|
||||
if (!fromYdoc) {
|
||||
// TODO_OFFLINE dynamic import.
|
||||
// Currently we have a constant list. Each project must have at least one bucket
|
||||
return getInitialBuckets()
|
||||
}
|
||||
// TODO_OFFLINE fill the `tasks`.
|
||||
return JSON.parse(fromStorage)
|
||||
return fromYdoc
|
||||
}
|
||||
|
||||
// /**
|
||||
|
@ -77,23 +83,23 @@ function getAllBucketsWithoutTasks(): IBucketWithoutTasks[] {
|
|||
// (bucket as IBucket).tasks = getAllTas
|
||||
// }
|
||||
|
||||
function getAllBuckets(): IBucket[] {
|
||||
const bucketsWithoutTasks = getAllBucketsWithoutTasks()
|
||||
const buckets = bucketsWithoutTasks.map(bucketWithoutTasks => {
|
||||
async function getAllBuckets(): Promise<IBucket[]> {
|
||||
const bucketsWithoutTasks = await getAllBucketsWithoutTasks()
|
||||
const buckets = bucketsWithoutTasks.map(async bucketWithoutTasks => {
|
||||
const b = bucketWithoutTasks as IBucket
|
||||
b.tasks = getTasksOfBucket(b.id)
|
||||
b.tasks = (await getTasksOfBucket(b.id))
|
||||
// Tasks are always sorted by their `kanbanPosition`.
|
||||
// https://kolaente.dev/vikunja/api/src/commit/6d8db0ce1e00e8c200a43b28ac98eb0fb825f4d4/pkg/models/kanban.go#L173-L178
|
||||
.sort((a, b) => a.kanbanPosition - b.kanbanPosition)
|
||||
return b
|
||||
})
|
||||
return buckets
|
||||
return Promise.all(buckets)
|
||||
}
|
||||
|
||||
export function getAllBucketsOfProject(projectId: number): IBucket[] {
|
||||
export async function getAllBucketsOfProject(projectId: number): Promise<IBucket[]> {
|
||||
// TODO_OFFLINE filter by position bruh.
|
||||
// TODO_OFFLINE perf: maybe it's not worth getting tasks of each project then in `getAllBuckets`.
|
||||
return getAllBuckets()
|
||||
return (await getAllBuckets())
|
||||
.filter(b => b.projectId === projectId)
|
||||
// Buckets are always sorted.
|
||||
// https://kolaente.dev/vikunja/api/src/commit/6d8db0ce1e00e8c200a43b28ac98eb0fb825f4d4/pkg/models/kanban.go#L139-L143
|
||||
|
@ -101,15 +107,17 @@ export function getAllBucketsOfProject(projectId: number): IBucket[] {
|
|||
}
|
||||
|
||||
/** https://kolaente.dev/vikunja/api/src/commit/769db0dab2e50bc477dec6c7e18309effc80a1bd/pkg/models/kanban.go#L80-L87 */
|
||||
export function getDefaultBucket(projectId: number): IBucket {
|
||||
return getAllBucketsOfProject(projectId).sort((a, b) => a.position - b.position)[0]
|
||||
export async function getDefaultBucket(projectId: number): Promise<IBucket> {
|
||||
return (await getAllBucketsOfProject(projectId))
|
||||
.sort((a, b) => a.position - b.position)
|
||||
[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* https://kolaente.dev/vikunja/api/src/commit/066c26f83e1e066bdc9d80b4642db1df0d6a77eb/pkg/models/kanban.go#L252-L267
|
||||
*/
|
||||
export function createBucket(bucket: IBucket): IBucket {
|
||||
const allBuckets = getAllBucketsWithoutTasks()
|
||||
export async function createBucket(bucket: IBucket): Promise<IBucket> {
|
||||
const allBuckets = await getAllBucketsWithoutTasks()
|
||||
const maxPosition = allBuckets.reduce((currMax, b) => {
|
||||
return b.position > currMax
|
||||
? b.position
|
||||
|
@ -126,13 +134,16 @@ export function createBucket(bucket: IBucket): IBucket {
|
|||
// It's not actually necessary FYI, it will just taske extra space in the storage.
|
||||
tasks: undefined,
|
||||
}
|
||||
allBuckets.push(newBucketFullDataToStore)
|
||||
localStorage.setItem('buckets', JSON.stringify(allBuckets))
|
||||
allBuckets.push(newBucketFullDataToStore);
|
||||
(await ensureCreateSyncedYDoc())
|
||||
.getMap('bucketsMap')
|
||||
.set('buckets', allBuckets)
|
||||
// localStorage.setItem('buckets', JSON.stringify(allBuckets))
|
||||
return newBucketFullData
|
||||
}
|
||||
|
||||
export function updateBucket(newBucketData: IBucket) {
|
||||
const allBuckets = getAllBucketsWithoutTasks()
|
||||
export async function updateBucket(newBucketData: IBucket) {
|
||||
const allBuckets = await getAllBucketsWithoutTasks()
|
||||
// TODO_OFFLINE looks like the real backend also filters by prjectId, but
|
||||
// since in localBackend all bucket `id`s are unique even between projects,
|
||||
// it's not necessary
|
||||
|
@ -142,13 +153,17 @@ export function updateBucket(newBucketData: IBucket) {
|
|||
return
|
||||
}
|
||||
// TODO_OFFLINE remove tasks.
|
||||
allBuckets.splice(targetBucketInd, 1, newBucketData)
|
||||
localStorage.setItem('buckets', JSON.stringify(allBuckets))
|
||||
newBucketData.tasks = undefined
|
||||
allBuckets.splice(targetBucketInd, 1, newBucketData);
|
||||
(await ensureCreateSyncedYDoc())
|
||||
.getMap('bucketsMap')
|
||||
.set('buckets', allBuckets)
|
||||
// localStorage.setItem('buckets', JSON.stringify(allBuckets))
|
||||
return newBucketData
|
||||
}
|
||||
|
||||
export function deleteBucket({ id }: { id: number }) {
|
||||
const allBuckets = getAllBucketsWithoutTasks()
|
||||
export async function deleteBucket({ id }: { id: number }) {
|
||||
const allBuckets = await getAllBucketsWithoutTasks()
|
||||
|
||||
if (allBuckets.length <= 1) {
|
||||
// Prevent removing the last bucket.
|
||||
|
@ -162,20 +177,37 @@ export function deleteBucket({ id }: { id: number }) {
|
|||
return
|
||||
}
|
||||
const projectId = allBuckets[targetBucketInd].projectId
|
||||
allBuckets.splice(targetBucketInd, 1)
|
||||
localStorage.setItem('buckets', JSON.stringify(allBuckets))
|
||||
allBuckets.splice(targetBucketInd, 1);
|
||||
// localStorage.setItem('buckets', JSON.stringify(allBuckets))
|
||||
|
||||
const ydoc = await ensureCreateSyncedYDoc()
|
||||
ydoc
|
||||
.getMap('bucketsMap')
|
||||
.set('buckets', allBuckets)
|
||||
|
||||
// Move all the tasks from this bucket to the default one.
|
||||
// https://kolaente.dev/vikunja/api/src/commit/6aadaaaffc1fff4a94e35e8fa3f6eab397cbc3ce/pkg/models/kanban.go#L349-L353
|
||||
const deletedBuckedId = id
|
||||
const defaultBucketId = getDefaultBucket(projectId).id
|
||||
const allTasks = getAllTasks()
|
||||
allTasks.forEach(t => {
|
||||
if (t.bucketId === deletedBuckedId) {
|
||||
t.bucketId = defaultBucketId
|
||||
}
|
||||
const defaultBucketId = (await getDefaultBucket(projectId)).id
|
||||
|
||||
// const allTasks = getAllTasks()
|
||||
// allTasks.forEach(t => {
|
||||
// if (t.bucketId === deletedBuckedId) {
|
||||
// t.bucketId = defaultBucketId
|
||||
// }
|
||||
// })
|
||||
// localStorage.setItem('tasks', JSON.stringify(allTasks))
|
||||
const tasksYarr = ydoc.getArray('tasks')
|
||||
ydoc.transact(() => {
|
||||
tasksYarr.forEach((t, i) => {
|
||||
if (t.bucketId === deletedBuckedId) {
|
||||
// TODO_OFFLINE not sure if it's ok to mutate the array while `forEach` ing it.
|
||||
t.bucketId = defaultBucketId
|
||||
tasksYarr.delete(i)
|
||||
tasksYarr.insert(i, [t])
|
||||
}
|
||||
})
|
||||
})
|
||||
localStorage.setItem('tasks', JSON.stringify(allTasks))
|
||||
|
||||
// TODO_OFFLINE idk what it's supposed to return
|
||||
return true
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
import * as Y from 'yjs'
|
||||
import { IndexeddbPersistence as YjsIndexeddbPersistence } from 'y-indexeddb'
|
||||
import type { Webxdc as WebxdcGeneric } from './webxdc'
|
||||
|
||||
type Webxdc = WebxdcGeneric<{
|
||||
update: number[],
|
||||
sender: typeof webxdc.selfAddr
|
||||
}>
|
||||
declare const webxdc: Webxdc;
|
||||
// declare global {
|
||||
// interface Window {
|
||||
// webxdc: Webxdc;
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* @returns resolves when all the pending updates have been applied
|
||||
*/
|
||||
function initWebxdcSyncProvider(ydoc: Y.Doc): Promise<void> {
|
||||
// TODO_OFFLINE I'm not sure at all if this is correct. Namely:
|
||||
// I'm not sure if it is right to never apply updates that we've already applied
|
||||
// in the previos sessions, as it looks like that we rely on `IndexeddbPersistence`
|
||||
// to save all the applied updates. What happens if it for some reason doesn't manage
|
||||
// to not save some update? In theore we'd have to download it from other peers then, right?
|
||||
// But we don't do it here.
|
||||
//
|
||||
// Better check how existing providers work, like `y-websocket`. Looks like they use
|
||||
// `y-protocols/sync`.
|
||||
|
||||
// const lastAppliedWebxdcUpdateSerialNum = localStorage.getItem('__lastAppliedWebxdcUpdate')
|
||||
const lastAppliedWebxdcUpdateSerialNum = 0
|
||||
|
||||
const setListenerP = webxdc.setUpdateListener(
|
||||
(update) => {
|
||||
// if (update.payload.sender === webxdc.selfAddr) {
|
||||
// return
|
||||
// }
|
||||
console.log('update', update.serial)
|
||||
// TODO_OFFLINE optimize this. Batch updates? ydoc.transact?
|
||||
Y.applyUpdate(ydoc, new Uint8Array(update.payload.update), 'webxdcUpdateHandler')
|
||||
// localStorage.setItem('__lastAppliedWebxdcUpdate', update.serial.toString())
|
||||
},
|
||||
lastAppliedWebxdcUpdateSerialNum
|
||||
? parseInt(lastAppliedWebxdcUpdateSerialNum)
|
||||
: 0,
|
||||
)
|
||||
|
||||
// TODO_OFFLINE throttle
|
||||
// TODO not sure if this reacts to the changes
|
||||
ydoc.on('update', (update, origin) => {
|
||||
if (origin === 'webxdcUpdateHandler') {
|
||||
return
|
||||
}
|
||||
// TODO_OFFLINE conversion to an ordinary array. Not good for performance.
|
||||
// Look up how other Yjs connectors do this. Or
|
||||
// https://docs.yjs.dev/api/document-updates#example-base64-encoding
|
||||
const serializableArray = [...(update as Uint8Array)]
|
||||
webxdc.sendUpdate({
|
||||
payload: {
|
||||
update: serializableArray,
|
||||
sender: webxdc.selfAddr
|
||||
},
|
||||
},
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
return setListenerP
|
||||
}
|
||||
|
||||
async function maybeInitWebrtcSync(ydoc: Y.Doc) {
|
||||
if (localStorage.getItem('sharingEnabled') !== 'true') {
|
||||
return
|
||||
}
|
||||
const sharingRoomName = localStorage.getItem('sharingRoomName')
|
||||
if (sharingRoomName == null) {
|
||||
return
|
||||
}
|
||||
const sharingRoomPassword = localStorage.getItem('sharingRoomPassword')
|
||||
if (sharingRoomPassword == null) {
|
||||
return
|
||||
}
|
||||
const { WebrtcProvider } = await import('y-webrtc')
|
||||
// To avoid collisions when people are lazy about coming up with a long enough name.
|
||||
const fullRoomName = `vikunja-${sharingRoomName}`
|
||||
const provider = new WebrtcProvider(fullRoomName, ydoc, { password: sharingRoomPassword })
|
||||
console.log('WebRTC sharing enabled')
|
||||
}
|
||||
|
||||
async function createSyncedYDoc(): Promise<Y.Doc> {
|
||||
const ydoc = new Y.Doc()
|
||||
|
||||
const indexeddbProvider = new YjsIndexeddbPersistence('p2p-tasks-db', ydoc)
|
||||
// Did not check if we actually need to await
|
||||
await indexeddbProvider.whenSynced
|
||||
|
||||
if (window.webxdc) {
|
||||
// TODO_OFFLINE dynamic import
|
||||
await initWebxdcSyncProvider(ydoc)
|
||||
} else {
|
||||
await maybeInitWebrtcSync(ydoc)
|
||||
}
|
||||
|
||||
|
||||
// const lastAppliedWebxdcUpdate = localStorage.getItem('__lastAppliedWebxdcUpdate')
|
||||
// // function onWebxdcUpdate()
|
||||
// const onWebxdcUpdate: Parameters<Webxdc['setUpdateListener']>[0] = (update) => {
|
||||
// // update.
|
||||
// }
|
||||
|
||||
return ydoc
|
||||
}
|
||||
|
||||
let _ydoc: Promise<Y.Doc> | undefined = undefined
|
||||
export const ensureCreateSyncedYDoc = () => {
|
||||
if (!_ydoc) {
|
||||
// TODO_OFFLINE fix: if you call it twice in a row before it resolves,
|
||||
// `createSyncedYDoc` will be exected twice. Don't await.
|
||||
_ydoc = createSyncedYDoc()
|
||||
}
|
||||
return _ydoc
|
||||
}
|
|
@ -1,7 +1,14 @@
|
|||
import type { ITask } from '@/modelTypes/ITask'
|
||||
import * as Y from 'yjs'
|
||||
import { getDefaultBucket } from './buckets'
|
||||
import { ensureCreateSyncedYDoc } from './sync'
|
||||
import { defaultPositionIfZero } from './utils/calculateDefaultPosition'
|
||||
|
||||
// createSyncedYDoc().then(d => {
|
||||
// window.ydoc = d
|
||||
// console.log(d)
|
||||
// })
|
||||
|
||||
// TODO_OFFLINE return types? ITask is not it, because of snake case and Date format.
|
||||
|
||||
// TODO_OFFLINE actually `project_id` is not always present on a task.
|
||||
|
@ -23,12 +30,16 @@ function getUniquieInt() {
|
|||
/**
|
||||
* Yes, we store the data in camelCase.
|
||||
*/
|
||||
export function getAllTasks(): ITask[] {
|
||||
const fromStorage = localStorage.getItem('tasks')
|
||||
if (!fromStorage) {
|
||||
return []
|
||||
}
|
||||
const tasks: ITask[] = JSON.parse(fromStorage)
|
||||
export async function getAllTasks(): Promise<ITask[]> {
|
||||
// const fromStorage = localStorage.getItem('tasks')
|
||||
// if (!fromStorage) {
|
||||
// return []
|
||||
// }
|
||||
// const tasks: ITask[] = JSON.parse(fromStorage)
|
||||
|
||||
const fromYdoc = (await ensureCreateSyncedYDoc()).getArray('tasks')
|
||||
|
||||
const tasks = fromYdoc.toArray() as ITask[]
|
||||
// TODO_OFFLINE don't just always sort them by position but look at
|
||||
// `parameters.sort_by`.
|
||||
return tasks.sort((a, b) => a.position - b.position)
|
||||
|
@ -37,26 +48,29 @@ export function getAllTasks(): ITask[] {
|
|||
// getAllTasksWithFilters(params)
|
||||
|
||||
// TODO_OFFLINE we only have one project currently, actually.
|
||||
export function getTasksOfProject<PID extends number>(projectId: PID): Array<ITask & { projectId: PID }> {
|
||||
const tasks: ITask[] = getAllTasks().filter(t => t.projectId === projectId)
|
||||
export async function getTasksOfProject<PID extends number>(projectId: PID): Promise<(ITask & { projectId: PID })[]> {
|
||||
const tasks: ITask[] = (await getAllTasks()).filter(t => t.projectId === projectId)
|
||||
return tasks
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsorted
|
||||
*/
|
||||
export function getTasksOfBucket<BID extends number>(
|
||||
export async function getTasksOfBucket<BID extends number>(
|
||||
bucketId: BID,
|
||||
): Array<ITask & { bucketId: BID }> {
|
||||
const tasks: ITask[] = getAllTasks().filter(t => t.bucketId === bucketId)
|
||||
): Promise<Array<ITask & { bucketId: BID} >> {
|
||||
const tasks: ITask[] = (await getAllTasks()).filter(t => t.bucketId === bucketId)
|
||||
return tasks
|
||||
}
|
||||
|
||||
/**
|
||||
* https://kolaente.dev/vikunja/api/src/commit/066c26f83e1e066bdc9d80b4642db1df0d6a77eb/pkg/models/tasks.go#L913-L990
|
||||
*/
|
||||
export function createTask(newTask: ITask): ITask {
|
||||
const allTasks = getAllTasks()
|
||||
export async function createTask(newTask: ITask): Promise<ITask> {
|
||||
|
||||
|
||||
|
||||
// const allTasks = getAllTasks()
|
||||
const newTaskFullData: ITask = {
|
||||
...newTask,
|
||||
id: Math.round(Math.random() * 1000000000000),
|
||||
|
@ -64,41 +78,97 @@ export function createTask(newTask: ITask): ITask {
|
|||
position: defaultPositionIfZero(newTask.position),
|
||||
kanbanPosition: defaultPositionIfZero(newTask.kanbanPosition),
|
||||
// https://kolaente.dev/vikunja/api/src/commit/769db0dab2e50bc477dec6c7e18309effc80a1bd/pkg/models/tasks.go#L939-L940
|
||||
bucketId: newTask.bucketId || getDefaultBucket(newTask.projectId).id,
|
||||
}
|
||||
allTasks.unshift(newTaskFullData)
|
||||
localStorage.setItem('tasks', JSON.stringify(allTasks))
|
||||
bucketId: newTask.bucketId > 0
|
||||
? newTask.bucketId
|
||||
: (await getDefaultBucket(newTask.projectId)).id,
|
||||
};
|
||||
|
||||
(await ensureCreateSyncedYDoc())
|
||||
.getArray('tasks')
|
||||
.unshift([newTaskFullData])
|
||||
// .unshift([new Y.Map(newTaskFullData)])
|
||||
|
||||
// (await allTasks).unshift(newTaskFullData)
|
||||
// localStorage.setItem('tasks', JSON.stringify(allTasks))
|
||||
return newTaskFullData
|
||||
}
|
||||
|
||||
export function getTask(taskId: number) {
|
||||
return getAllTasks().find(t => t.id === taskId)
|
||||
export async function getTask(taskId: number) {
|
||||
return (await getAllTasks()).find(t => t.id === taskId)
|
||||
}
|
||||
|
||||
export function updateTask(newTaskData: ITask) {
|
||||
export async function updateTask(newTaskData: ITask) {
|
||||
// TODO_OFFLINE a lot of stuff is not implemented. For example, marking a task "done"
|
||||
// when it is moved to the "done" bucket.
|
||||
// https://kolaente.dev/vikunja/api/src/commit/6aadaaaffc1fff4a94e35e8fa3f6eab397cbc3ce/pkg/models/tasks.go#L1008
|
||||
const allTasks = getAllTasks()
|
||||
const targetTaskInd = allTasks.findIndex(t => t.id === newTaskData.id)
|
||||
|
||||
|
||||
// let targetTask
|
||||
// const asd = (await ensureCreateSyncedYDoc()).getArray('tasks').forEach(_t => {
|
||||
// // const t = _t as Y.Map<ITask>
|
||||
// const t = _t
|
||||
// if (t.get('id') === newTaskData.id) {
|
||||
// targetTask = t
|
||||
// }
|
||||
// })
|
||||
|
||||
const tasksArr = (await ensureCreateSyncedYDoc()).getArray('tasks')
|
||||
|
||||
let targetTaskInd = -1
|
||||
tasksArr.forEach((_t, i) => {
|
||||
// const t = _t as Y.Map<ITask>
|
||||
const t = _t
|
||||
if (t.id === newTaskData.id) {
|
||||
targetTaskInd = i;
|
||||
}
|
||||
})
|
||||
|
||||
// const allTasks = await getAllTasks()
|
||||
// const targetTaskInd = allTasks.findIndex(t => t.id === newTaskData.id)
|
||||
|
||||
if (targetTaskInd < 0) {
|
||||
console.warn('Tried to update a task, but it does not exist')
|
||||
return
|
||||
}
|
||||
allTasks.splice(targetTaskInd, 1, newTaskData)
|
||||
localStorage.setItem('tasks', JSON.stringify(allTasks))
|
||||
|
||||
tasksArr.doc!.transact(() => {
|
||||
// TODO_OFFLINE consider mutating the task instead.
|
||||
tasksArr.delete(targetTaskInd)
|
||||
tasksArr.insert(targetTaskInd, [newTaskData])
|
||||
})
|
||||
|
||||
// allTasks.splice(targetTaskInd, 1, newTaskData)
|
||||
// localStorage.setItem('tasks', JSON.stringify(allTasks))
|
||||
return newTaskData
|
||||
}
|
||||
|
||||
export function deleteTask({ id }: { id: number }) {
|
||||
const allTasks = getAllTasks()
|
||||
const targetTaskInd = allTasks.findIndex(t => t.id === id)
|
||||
export async function deleteTask({ id }: { id: number }) {
|
||||
const tasksArr = (await ensureCreateSyncedYDoc()).getArray('tasks')
|
||||
|
||||
let targetTaskInd = -1
|
||||
tasksArr.forEach((_t, i) => {
|
||||
// const t = _t as Y.Map<ITask>
|
||||
const t = _t
|
||||
if (t.id === id) {
|
||||
targetTaskInd = i;
|
||||
}
|
||||
})
|
||||
|
||||
if (targetTaskInd < 0) {
|
||||
console.warn('Tried to delete a task, but it does not exist')
|
||||
return
|
||||
}
|
||||
allTasks.splice(targetTaskInd, 1)
|
||||
localStorage.setItem('tasks', JSON.stringify(allTasks))
|
||||
|
||||
// tasksArr.delete(targetTaskInd)
|
||||
|
||||
// const allTasks = await getAllTasks()
|
||||
// const targetTaskInd = allTasks.findIndex(t => t.id === id)
|
||||
// if (targetTaskInd < 0) {
|
||||
// console.warn('Tried to delete a task, but it does not exist')
|
||||
// return
|
||||
// }
|
||||
// allTasks.splice(targetTaskInd, 1)
|
||||
// localStorage.setItem('tasks', JSON.stringify(allTasks))
|
||||
|
||||
// TODO_OFFLINE idk what it's supposed to return
|
||||
return true
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
//@ts-check
|
||||
|
||||
type SendingStatusUpdate<T> = {
|
||||
/** the payload, deserialized json:
|
||||
* any javascript primitive, array or object. */
|
||||
payload: T;
|
||||
/** optional, short, informational message that will be added to the chat,
|
||||
* eg. "Alice voted" or "Bob scored 123 in MyGame";
|
||||
* usually only one line of text is shown,
|
||||
* use this option sparingly to not spam the chat. */
|
||||
info?: string;
|
||||
/** optional, if the Webxdc creates a document, you can set this to the name of the document;
|
||||
* do not set if the Webxdc does not create a document */
|
||||
document?: string;
|
||||
/** optional, short text, shown beside the icon;
|
||||
* it is recommended to use some aggregated value,
|
||||
* eg. "8 votes", "Highscore: 123" */
|
||||
summary?: string;
|
||||
};
|
||||
|
||||
type ReceivedStatusUpdate<T> = {
|
||||
/** the payload, deserialized json */
|
||||
payload: T;
|
||||
/** the serial number of this update. Serials are larger than 0 and newer serials have higher numbers */
|
||||
serial: number;
|
||||
/** the maximum serial currently known */
|
||||
max_serial: number;
|
||||
/** optional, short, informational message. */
|
||||
info?: string;
|
||||
/** optional, if the Webxdc creates a document, this is the name of the document;
|
||||
* not set if the Webxdc does not create a document */
|
||||
document?: string;
|
||||
/** optional, short text, shown beside the webxdc's icon. */
|
||||
summary?: string;
|
||||
};
|
||||
|
||||
interface Webxdc<T> {
|
||||
/** Returns the peer's own address.
|
||||
* This is esp. useful if you want to differ between different peers - just send the address along with the payload,
|
||||
* and, if needed, compare the payload addresses against selfAddr() later on. */
|
||||
selfAddr: string;
|
||||
/** Returns the peer's own name. This is name chosen by the user in their settings, if there is nothing set, that defaults to the peer's address. */
|
||||
selfName: string;
|
||||
/**
|
||||
* set a listener for new status updates.
|
||||
* The "serial" specifies the last serial that you know about (defaults to 0).
|
||||
* Note that own status updates, that you send with {@link sendUpdate}, also trigger this method
|
||||
* @returns promise that resolves when the listener has processed all the update messages known at the time when `setUpdateListener` was called.
|
||||
* */
|
||||
setUpdateListener(cb: (statusUpdate: ReceivedStatusUpdate<T>) => void, serial?: number): Promise<void>;
|
||||
/**
|
||||
* @deprecated See {@link setUpdateListener|`setUpdateListener()`}.
|
||||
*/
|
||||
getAllUpdates(): Promise<ReceivedStatusUpdate<T>[]>;
|
||||
/**
|
||||
* Webxdc are usually shared in a chat and run independently on each peer. To get a shared status, the peers use sendUpdate() to send updates to each other.
|
||||
* @param update status update to send
|
||||
* @param description short, human-readable description what this update is about. this is shown eg. as a fallback text in an email program.
|
||||
*/
|
||||
sendUpdate(update: SendingStatusUpdate<T>, description: string): void;
|
||||
}
|
||||
|
||||
////////// ANCHOR: global
|
||||
declare global {
|
||||
interface Window {
|
||||
webxdc: Webxdc<any>;
|
||||
}
|
||||
}
|
||||
////////// ANCHOR_END: global
|
||||
|
||||
export { SendingStatusUpdate, ReceivedStatusUpdate, Webxdc };
|
|
@ -1,5 +1,39 @@
|
|||
<template>
|
||||
<card :title="$t('user.settings.general.title')" class="general-settings" :loading="loading">
|
||||
<template v-if="isWebxdc">
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="sharingEnabled"/>
|
||||
Enable sharing
|
||||
</label>
|
||||
<!-- TODO_OFFLINE a way to have multiple rooms, some private. -->
|
||||
<p>Everyone who knows the room name and the password will have read and write access to the data over network.</p>
|
||||
<p>You need to reload the page after changing the sharing settings.</p>
|
||||
</div>
|
||||
<template v-if="sharingEnabled">
|
||||
<div class="field">
|
||||
<label class="is-flex is-align-items-center">
|
||||
Sharing: room name
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
required
|
||||
v-model="sharingRoomName"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="is-flex is-align-items-center">
|
||||
Sharing: room password
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
v-model="sharingRoomPassword"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="playSoundWhenDone"/>
|
||||
|
@ -139,6 +173,11 @@ function getPlaySoundWhenDoneSetting() {
|
|||
}
|
||||
|
||||
const playSoundWhenDone = ref(getPlaySoundWhenDoneSetting())
|
||||
// TODO_OFFLINE refactor: DRY
|
||||
const isWebxdc = window.webxdc == undefined
|
||||
const sharingEnabled = ref(localStorage.getItem('sharingEnabled') === 'true')
|
||||
const sharingRoomName = ref(localStorage.getItem('sharingRoomName') ?? '')
|
||||
const sharingRoomPassword = ref(localStorage.getItem('sharingRoomPassword') ?? '')
|
||||
const quickAddMagicMode = ref(getQuickAddMagicMode())
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
@ -178,6 +217,13 @@ watch(
|
|||
|
||||
async function updateSettings() {
|
||||
localStorage.setItem(playSoundWhenDoneKey, playSoundWhenDone.value ? 'true' : 'false')
|
||||
localStorage.setItem('sharingEnabled', sharingEnabled.value ? 'true' : 'false')
|
||||
if (sharingRoomName.value.length > 0) {
|
||||
localStorage.setItem('sharingRoomName', sharingRoomName.value)
|
||||
}
|
||||
if (sharingRoomPassword.value.length > 0) {
|
||||
localStorage.setItem('sharingRoomPassword', sharingRoomPassword.value)
|
||||
}
|
||||
setQuickAddMagicMode(quickAddMagicMode.value)
|
||||
|
||||
await authStore.saveUserSettings({
|
||||
|
|
|
@ -187,6 +187,9 @@ export default defineConfig(({mode}) => {
|
|||
build: {
|
||||
target: 'esnext',
|
||||
rollupOptions: {
|
||||
external: [
|
||||
"/webxdc.js"
|
||||
],
|
||||
plugins: [
|
||||
visualizer({
|
||||
filename: 'stats.html',
|
||||
|
|
Loading…
Reference in New Issue