Compare commits
4 Commits
08dda6c6b0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ec4912aa4a | |||
| 0b3d1d5ed0 | |||
| 229b6b6f7a | |||
| 63e92756fe |
@@ -2,7 +2,7 @@
|
|||||||
# file: config/version.py
|
# file: config/version.py
|
||||||
import os
|
import os
|
||||||
|
|
||||||
BASE_VERSION = "1.0.15" # update manually when releasing features
|
BASE_VERSION = "1.0.16" # update manually when releasing features
|
||||||
|
|
||||||
def get_full_version() -> str:
|
def get_full_version() -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "DxpdHN19-u06WvFHt0QDGQ-L8vPbVjFgShFhxKNhBMI",
|
"value": "reYniSI2OIXUAcXkeBNTaaOD7MJOzNEhwxwSDO42bew",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1787085982.145659,
|
"expires": 1787504746.987795,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiJkYzkwNjU3Mi02MmNkLTRjM2MtOTFiNy0wZGJjZGVhMjZjZDMiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzkzMjA3ODJ9.8tmlxpVR4KL_kclP1Ak1Wdx7_qZtLayb3lrcQHlFdY8",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiJhNDQyY2E4Ni1lNGIzLTRjZjEtYThmYS0zNWJmYmZhNzk5NjYiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzk3Mzk1NDZ9.3zqjc9RdG9jpr5wbmhbqYUCvzxUl9d8tcc4EY1-BYJc",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1779320782.145583,
|
"expires": 1779739546.987748,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1779309981997}"
|
"value": "{\"type\":\"logout\",\"at\":1779728746835}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1779482782305}"
|
"value": "{\"expiresAt\":1779901547148}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "fzE1WOI8ku1O7zv_5fFC_eRgk0CmbokqVZk1g_M_V9A",
|
"value": "DlW9UoiIvVxuPPp3wfo3qsVP5tlO2Iw2e8TZThWeMDw",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1787085982.03234,
|
"expires": 1787504746.877459,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiZjQwZjEzMDgtNmNjNi00NTA4LWE0NmQtODYwMjMzNzhkNDQ4IiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc5MzIwNzgyfQ.NI2FXDnR2Q7yoKSMjFeElmQNtHDYoXtB8zQ-nPda2NQ",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiM2E3ZDA3NjYtYTQyNy00NTQ5LWE0NGEtMWU0ZjUwOGRhZDBhIiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc5NzM5NTQ2fQ.jFLpGeJBM7U-x-N1jT-muHjBStXFyeb5oQS0LryWwr0",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1779320782.032289,
|
"expires": 1779739546.877411,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1779309981887}"
|
"value": "{\"type\":\"logout\",\"at\":1779728746722}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1779482782189}"
|
"value": "{\"expiresAt\":1779901547028}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "UEtX_GEYexwXzL3sl4-QgOEsdnqThwUccA82UsoiR2M",
|
"value": "Xz4WQkQNdryfFsS3GaA09dGDz3yEaRwHxF2VdIh8LwQ",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1787085980.357146,
|
"expires": 1787504745.201123,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiJlNDlkMWU2Mi1kNDFiLTQ2YTEtYTU3ZS1lNmUwMzk0ZmEwYjgiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzkzMjA3ODB9.waOPPnZ7gmCgOUxGU9CO7APhdpr78mDJCokRlbfLV3w",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiJhMWVlZGNkOC1hMGE0LTQyYjMtOWU3ZC01MWRmNGM1ZTFiNTUiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzk3Mzk1NDV9.Q2q-xiDjAw8pu3t8ioaawUpGK1__5wBiPZ-vFvXHnmw",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1779320780.357103,
|
"expires": 1779739545.201078,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1779309980235}"
|
"value": "{\"type\":\"logout\",\"at\":1779728745058}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1779482780489}"
|
"value": "{\"expiresAt\":1779901545328}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ test.describe('Routine item drag-to-reorder', () => {
|
|||||||
await page.goto(`/parent/tasks/routines/${routineId}/edit`)
|
await page.goto(`/parent/tasks/routines/${routineId}/edit`)
|
||||||
await page.locator('.item-row').first().waitFor({ state: 'visible' })
|
await page.locator('.item-row').first().waitFor({ state: 'visible' })
|
||||||
|
|
||||||
await page.locator('.item-row').nth(0).dragTo(page.locator('.item-row').nth(2))
|
await page.locator('.item-row').nth(0).locator('.drag-handle').dragTo(page.locator('.item-row').nth(2))
|
||||||
|
|
||||||
const names = await page.locator('.item-name').allTextContents()
|
const names = await page.locator('.item-name').allTextContents()
|
||||||
expect(names).toEqual([ITEM_B, ITEM_C, ITEM_A])
|
expect(names).toEqual([ITEM_B, ITEM_C, ITEM_A])
|
||||||
@@ -58,7 +58,7 @@ test.describe('Routine item drag-to-reorder', () => {
|
|||||||
await page.locator('.item-row').first().waitFor({ state: 'visible' })
|
await page.locator('.item-row').first().waitFor({ state: 'visible' })
|
||||||
|
|
||||||
// Drag first to last: [A, B, C] → [B, C, A]
|
// Drag first to last: [A, B, C] → [B, C, A]
|
||||||
await page.locator('.item-row').nth(0).dragTo(page.locator('.item-row').nth(2))
|
await page.locator('.item-row').nth(0).locator('.drag-handle').dragTo(page.locator('.item-row').nth(2))
|
||||||
|
|
||||||
// Confirm the drag updated the DOM before saving
|
// Confirm the drag updated the DOM before saving
|
||||||
const afterDrag = await page.locator('.item-name').allTextContents()
|
const afterDrag = await page.locator('.item-name').allTextContents()
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ const triggerTask = async (task: ChildTask) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleRoutineClick = (routine: ChildRoutine) => {
|
const handleRoutineClick = (routine: ChildRoutine) => {
|
||||||
|
if (isRoutineExpired(routine) || routine.pending_status === 'approved') return
|
||||||
dialogRoutine.value = routine
|
dialogRoutine.value = routine
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showRoutineOverlay.value = true
|
showRoutineOverlay.value = true
|
||||||
@@ -733,6 +734,7 @@ onUnmounted(() => {
|
|||||||
<span v-if="item.pending_status === 'pending'" class="chore-stamp pending-stamp"
|
<span v-if="item.pending_status === 'pending'" class="chore-stamp pending-stamp"
|
||||||
>PENDING</span
|
>PENDING</span
|
||||||
>
|
>
|
||||||
|
<span v-else-if="isRoutineExpired(item)" class="chore-stamp">TOO LATE</span>
|
||||||
<span v-else-if="item.pending_status === 'approved'" class="chore-stamp completed-stamp"
|
<span v-else-if="item.pending_status === 'approved'" class="chore-stamp completed-stamp"
|
||||||
>APPROVED</span
|
>APPROVED</span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -26,14 +26,10 @@
|
|||||||
v-for="(item, idx) in items"
|
v-for="(item, idx) in items"
|
||||||
:key="item.id || `new-${idx}`"
|
:key="item.id || `new-${idx}`"
|
||||||
class="item-row"
|
class="item-row"
|
||||||
|
:data-idx="idx"
|
||||||
:class="{ 'drag-over': dragOverIdx === idx, dragging: draggingIdx === idx }"
|
:class="{ 'drag-over': dragOverIdx === idx, dragging: draggingIdx === idx }"
|
||||||
draggable="true"
|
|
||||||
@dragstart="(e) => onDragStart(e, idx)"
|
|
||||||
@dragover.prevent="(e) => onDragOver(e, idx)"
|
|
||||||
@drop.prevent="onDrop(idx)"
|
|
||||||
@dragend="onDragEnd"
|
|
||||||
>
|
>
|
||||||
<span class="drag-handle" title="Drag to reorder">⠿</span>
|
<span class="drag-handle" title="Drag to reorder" @pointerdown.prevent="(e) => onPointerDown(e, idx)">⠿</span>
|
||||||
<div class="item-left">
|
<div class="item-left">
|
||||||
<img
|
<img
|
||||||
v-if="item.image_url"
|
v-if="item.image_url"
|
||||||
@@ -333,34 +329,51 @@ function cancelEditItem() {
|
|||||||
newItemLocalFile.value = null
|
newItemLocalFile.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragStart(e: DragEvent, idx: number) {
|
function onPointerDown(e: PointerEvent, idx: number) {
|
||||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'
|
|
||||||
cancelEditItem()
|
|
||||||
draggingIdx.value = idx
|
draggingIdx.value = idx
|
||||||
|
cancelEditItem()
|
||||||
|
|
||||||
|
const handlePointerMove = (moveEvt: PointerEvent) => {
|
||||||
|
moveEvt.preventDefault()
|
||||||
|
const el = document.elementFromPoint(moveEvt.clientX, moveEvt.clientY) as Element | null
|
||||||
|
if (!el) return
|
||||||
|
const row = el.closest('[data-idx]') as HTMLElement | null
|
||||||
|
if (!row) return
|
||||||
|
const rowIdx = parseInt(row.dataset.idx ?? '-1')
|
||||||
|
if (rowIdx !== -1 && rowIdx !== draggingIdx.value) {
|
||||||
|
dragOverIdx.value = rowIdx
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragOver(e: DragEvent, idx: number) {
|
const cleanup = (upEvt?: PointerEvent) => {
|
||||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'
|
if (upEvt) {
|
||||||
if (draggingIdx.value === null || draggingIdx.value === idx) return
|
const el = document.elementFromPoint(upEvt.clientX, upEvt.clientY) as Element | null
|
||||||
dragOverIdx.value = idx
|
const fromIdx = draggingIdx.value
|
||||||
}
|
if (el && fromIdx !== null) {
|
||||||
|
const row = el.closest('[data-idx]') as HTMLElement | null
|
||||||
function onDrop(idx: number) {
|
if (row) {
|
||||||
if (draggingIdx.value === null || draggingIdx.value === idx) {
|
const toIdx = parseInt(row.dataset.idx ?? '-1')
|
||||||
draggingIdx.value = null
|
if (toIdx !== -1 && toIdx !== fromIdx) {
|
||||||
dragOverIdx.value = null
|
const moved = items.value.splice(fromIdx, 1)[0]
|
||||||
return
|
items.value.splice(toIdx, 0, moved)
|
||||||
}
|
|
||||||
const moved = items.value.splice(draggingIdx.value, 1)[0]
|
|
||||||
items.value.splice(idx, 0, moved)
|
|
||||||
normalizeItemOrder()
|
normalizeItemOrder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
draggingIdx.value = null
|
draggingIdx.value = null
|
||||||
dragOverIdx.value = null
|
dragOverIdx.value = null
|
||||||
|
document.removeEventListener('pointermove', handlePointerMove)
|
||||||
|
document.removeEventListener('pointerup', handlePointerUp)
|
||||||
|
document.removeEventListener('pointercancel', handlePointerCancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragEnd() {
|
const handlePointerUp = (upEvt: PointerEvent) => cleanup(upEvt)
|
||||||
draggingIdx.value = null
|
const handlePointerCancel = () => cleanup()
|
||||||
dragOverIdx.value = null
|
|
||||||
|
document.addEventListener('pointermove', handlePointerMove, { passive: false })
|
||||||
|
document.addEventListener('pointerup', handlePointerUp)
|
||||||
|
document.addEventListener('pointercancel', handlePointerCancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadImage(file: File): Promise<string> {
|
async function uploadImage(file: File): Promise<string> {
|
||||||
@@ -532,8 +545,9 @@ function handleCancel() {
|
|||||||
cursor: grab;
|
cursor: grab;
|
||||||
color: var(--form-label, #aaa);
|
color: var(--form-label, #aaa);
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
padding-right: 0.45rem;
|
padding: 0.4rem 0.45rem 0.4rem 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user