Compare commits

...

11 Commits
v27 ... main

Author SHA1 Message Date
Magnus Åhall
60f20a754f Removed unnecessary debug log 2024-04-19 18:44:12 +02:00
Magnus Åhall
c132f495b4 Added back a deleted URL character 2024-04-19 18:42:41 +02:00
Magnus Åhall
bcce516c66 Fixed calendar aspect ratio to my liking 2024-04-19 18:36:41 +02:00
Magnus Åhall
e9967ebdc6 Bumped to v29 2024-04-19 18:32:44 +02:00
Magnus Åhall
0a6eaed89f #8, added fullcalendar 2024-04-19 18:32:37 +02:00
Magnus Åhall
1a9d532d02 Fixed #7 2024-04-19 15:35:16 +02:00
d9fa6fd477 Update README.md 2024-04-19 13:34:30 +00:00
Magnus Åhall
8039dfaf42 Bumped to v28 2024-04-18 20:08:25 +02:00
Magnus Åhall
0dcc1e1fd9 Fixed bug deleting all schedules 2024-04-18 20:08:07 +02:00
Magnus Åhall
9d45d87ef3 #6, initial continuous adding of items 2024-04-18 07:47:35 +02:00
Magnus Åhall
2c16d7af60 Removed unnecessary logging 2024-04-17 18:48:25 +02:00
11 changed files with 331 additions and 20 deletions

View File

@ -8,6 +8,10 @@ Create an empty database. The configured user needs to be able to create and alt
Create a configuration file `$HOME/.config/notes.yaml` with the following content:
```yaml
network:
address: '[::]'
port: 1371
websocket:
domains:
- notes.com

View File

@ -761,7 +761,9 @@ func scheduleList(w http.ResponseWriter, r *http.Request, sess *session.T) { //
w.Header().Add("Access-Control-Allow-Origin", "*")
request := struct {
NodeID int
NodeID int
StartDate time.Time
EndDate time.Time
}{}
body, _ := io.ReadAll(r.Body)
if len(body) > 0 {
@ -773,14 +775,14 @@ func scheduleList(w http.ResponseWriter, r *http.Request, sess *session.T) { //
}
var schedules []Schedule
schedules, err = FutureSchedules(sess.UserID, request.NodeID)
schedules, err = FutureSchedules(sess.UserID, request.NodeID, request.StartDate, request.EndDate)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"OK": true,
"ScheduleEvents": schedules,
})
} // }}}

View File

@ -325,6 +325,10 @@ func CreateNode(userID, parentID int, name string) (node Node, err error) { // {
return
} // }}}
func UpdateNode(userID, nodeID, timeOffset int, content string, cryptoKeyID int, markdown bool) (err error) { // {{{
if nodeID == 0 {
return
}
var timezone string
row := service.Db.Conn.QueryRow(`SELECT timezone FROM _webservice.user WHERE id=$1`, userID)
err = row.Scan(&timezone)

View File

@ -36,7 +36,6 @@ func scheduleHandler() { // {{{
tick := time.NewTicker(time.Minute)
for {
schedules := ExpiredSchedules()
logger.Info("FOO", "schedules", schedules)
for _, event := range schedules {
notificationManager.Send(
event.UserID,
@ -216,9 +215,17 @@ func ExpiredSchedules() (schedules []Schedule) { // {{{
}
return
} // }}}
func FutureSchedules(userID int, nodeID int) (schedules []Schedule, err error) {// {{{
func FutureSchedules(userID int, nodeID int, start time.Time, end time.Time) (schedules []Schedule, err error) { // {{{
schedules = []Schedule{}
var foo string
row := service.Db.Conn.QueryRow(`SELECT TO_CHAR($1::date AT TIME ZONE 'UTC', 'yyyy-mm-dd HH24:MI')`, start)
err = row.Scan(&foo)
if err != nil {
return
}
logger.Info("FOO", "date", foo)
res := service.Db.Conn.QueryRow(`
WITH schedule_events AS (
SELECT
@ -230,7 +237,7 @@ func FutureSchedules(userID int, nodeID int) (schedules []Schedule, err error) {
'updated', n.updated
) AS node,
s.schedule_uuid,
time AT TIME ZONE 'UTC' AS time,
time AT TIME ZONE u.timezone AS time,
s.description,
s.acknowledged,
s.remind_minutes AS RemindMinutes
@ -244,6 +251,14 @@ func FutureSchedules(userID int, nodeID int) (schedules []Schedule, err error) {
WHEN $2 > 0 THEN n.id = $2
ELSE true
END
) AND (
CASE WHEN TO_CHAR($3::date, 'yyyy-mm-dd HH24:MI') = '0001-01-01 00:00' THEN TRUE
ELSE (s.time AT TIME ZONE u.timezone) >= $3
END
) AND (
CASE WHEN TO_CHAR($4::date, 'yyyy-mm-dd HH24:MI') = '0001-01-01 00:00' THEN TRUE
ELSE (s.time AT TIME ZONE u.timezone) <= $4
END
) AND
time >= NOW() AND
NOT acknowledged
@ -252,8 +267,10 @@ func FutureSchedules(userID int, nodeID int) (schedules []Schedule, err error) {
COALESCE(jsonb_agg(s.*), '[]'::jsonb)
FROM schedule_events s
`,
userID,
nodeID,
userID,
nodeID,
start,
end,
)
var j []byte
err = res.Scan(&j)
@ -269,4 +286,4 @@ func FutureSchedules(userID int, nodeID int) (schedules []Schedule, err error) {
}
return
}// }}}
} // }}}

View File

@ -13,6 +13,11 @@ html {
*:after {
box-sizing: inherit;
}
*,
*:focus,
*:hover {
outline: none;
}
[onClick] {
cursor: pointer;
}
@ -413,6 +418,7 @@ header .menu {
cursor: pointer;
}
#checklist .checklist-item {
transform: translate(0, 0);
display: grid;
grid-template-columns: repeat(3, min-content);
grid-gap: 0 8px;
@ -778,6 +784,56 @@ header .menu {
#schedule-events .header {
font-weight: bold;
}
#input-text {
border: 1px solid #000 !important;
padding: 16px;
width: 300px;
}
#input-text .label {
margin-bottom: 4px;
}
#input-text input[type=text] {
width: 100%;
padding: 4px;
}
#input-text .buttons {
display: grid;
grid-template-columns: 1fr 64px 64px;
grid-gap: 8px;
margin-top: 8px;
}
#fullcalendar {
margin: 32px;
color: #444;
}
.folder .tabs {
border-left: 1px solid #888;
display: flex;
}
.folder .tabs .tab {
padding: 16px 32px;
border-top: 1px solid #888;
border-bottom: 1px solid #888;
border-right: 1px solid #888;
color: #444;
background: #eee;
cursor: pointer;
}
.folder .tabs .tab.selected {
border-bottom: none;
background: #fff;
}
.folder .tabs .hack {
border-bottom: 1px solid #888;
width: 100%;
}
.folder .content {
padding-top: 1px;
border-left: 1px solid #888;
border-right: 1px solid #888;
border-bottom: 1px solid #888;
padding-bottom: 1px;
}
@media only screen and (max-width: 932px) {
#app.node {
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "schedule" "files" "blank";

View File

@ -27,6 +27,7 @@
</script>
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/sjcl.js"></script>
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js"></script>
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/fullcalendar.min.js"></script>
</head>
<body>

View File

@ -110,10 +110,11 @@ export class Checklist extends Component {
this.groupElements = {}
this.state = {
confirmDeletion: true,
continueAddingItems: true,
}
window._checklist = this
}//}}}
render({ ui, groups }, { confirmDeletion }) {//{{{
render({ ui, groups }, { confirmDeletion, continueAddingItems }) {//{{{
this.groupElements = {}
if (groups.length == 0 && !ui.node.value.ShowChecklist.value)
return
@ -136,6 +137,10 @@ export class Checklist extends Component {
<input type="checkbox" id="confirm-checklist-delete" checked=${confirmDeletion} onchange=${() => this.setState({ confirmDeletion: !confirmDeletion })} />
<label for="confirm-checklist-delete">Confirm checklist deletion</label>
</div>
<div>
<input type="checkbox" id="continue-adding-items" checked=${continueAddingItems} onchange=${() => this.setState({ continueAddingItems: !continueAddingItems })} />
<label for="continue-adding-items">Continue adding items</label>
</div>
`
}
@ -194,10 +199,62 @@ export class Checklist extends Component {
}//}}}
}
class InputElement extends Component {
render({ placeholder, label }) {//{{{
return html`
<dialog id="input-text">
<div class="container">
<div class="label">${label}</div>
<input id="input-text-el" type="text" placeholder=${placeholder} />
<div class="buttons">
<div></div>
<button onclick=${()=>this.cancel()}>Cancel</button>
<button onclick=${()=>this.ok()}>OK</button>
</div>
</div>
</dialog>
`
}//}}}
componentDidMount() {//{{{
const dlg = document.getElementById('input-text')
const input = document.getElementById('input-text-el')
dlg.showModal()
dlg.addEventListener("keydown", evt => this.keyhandler(evt))
input.addEventListener("keydown", evt => this.keyhandler(evt))
input.focus()
}//}}}
ok() {//{{{
const input = document.getElementById('input-text-el')
this.props.callback(true, input.value)
}//}}}
cancel() {//{{{
this.props.callback(false)
}//}}}
keyhandler(evt) {//{{{
let handled = true
switch (evt.key) {
case 'Enter':
this.ok()
break;
case 'Escape':
this.cancel()
break;
default:
handled = false
}
if (handled) {
evt.stopPropagation()
evt.preventDefault()
}
}//}}}
}
class ChecklistGroupElement extends Component {
constructor() {//{{{
super()
this.label = createRef()
this.addingItem = signal(false)
}//}}}
render({ ui, group }) {//{{{
let items = ({ ui, group }) =>
@ -206,30 +263,42 @@ class ChecklistGroupElement extends Component {
.map(item => html`<${ChecklistItemElement} key="item-${item.ID}" ui=${ui} group=${this} item=${item} />`)
let label = () => html`<div class="label" style="cursor: pointer" ref=${this.label} onclick=${() => this.editLabel()}>${group.Label}</div>`
let addItem = () => {
if (this.addingItem.value)
return html`<${InputElement} label="New item" callback=${(ok, val) => this.addItem(ok, val)} />`
}
return html`
<${addItem} />
<div class="checklist-group-container">
<div class="checklist-group ${ui.edit.value ? 'edit' : ''}">
<div class="reorder" style="cursor: grab"></div>
<img src="/images/${_VERSION}/trashcan.svg" onclick=${() => this.delete()} />
<${label} />
<img src="/images/${_VERSION}/add-gray.svg" onclick=${() => this.addItem()} />
<img src="/images/${_VERSION}/add-gray.svg" onclick=${() => this.addingItem.value = true} />
</div>
<${items} ui=${ui} group=${group} />
</div>
`
}//}}}
addItem() {//{{{
let label = prompt("Create a new item")
if (label === null)
addItem(ok, label) {//{{{
if (!ok) {
this.addingItem.value = false
return
}
label = label.trim()
if (label == '')
if (label == '') {
this.addingItem.value = false
return
}
this.props.group.addItem(label, () => {
this.forceUpdate()
})
if (!this.props.ui.state.continueAddingItems)
this.addingItem.value = false
}//}}}
editLabel() {//{{{
let label = prompt('Edit label', this.props.group.Label)
@ -299,7 +368,7 @@ class ChecklistItemElement extends Component {
`
}//}}}
componentDidMount() {//{{{
this.base.addEventListener('dragstart', () => this.dragStart())
this.base.addEventListener('dragstart', evt => this.dragStart(evt))
this.base.addEventListener('dragend', () => this.dragEnd())
this.base.addEventListener('dragenter', evt => this.dragEnter(evt))
}//}}}
@ -353,10 +422,13 @@ class ChecklistItemElement extends Component {
setDragTarget(state) {//{{{
this.setState({ dragTarget: state })
}//}}}
dragStart() {//{{{
dragStart(evt) {//{{{
// Shouldn't be needed, but in case the previous drag was bungled up, we reset.
this.props.ui.dragReset()
this.props.ui.dragItemSource = this
const img = new Image();
evt.dataTransfer.setDragImage(img, 10, 10);
}//}}}
dragEnter(evt) {//{{{
evt.preventDefault()
@ -393,7 +465,7 @@ class ChecklistItemElement extends Component {
this.props.ui.groupElements[fromGroup.ID].current.forceUpdate()
this.props.ui.groupElements[toGroup.ID].current.forceUpdate()
from.move(to, ()=>console.log('ok'))
from.move(to, () => {})
}//}}}
}

6
static/js/lib/fullcalendar.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -986,6 +986,41 @@ class ProfileSettings extends Component {
}
class ScheduleEventList extends Component {
static CALENDAR = Symbol('CALENDAR')
static LIST = Symbol('LIST')
constructor() {//{{{
super()
this.tab = signal(ScheduleEventList.CALENDAR)
}//}}}
render() {//{{{
var tab
switch (this.tab.value) {
case ScheduleEventList.CALENDAR:
tab = html`<${ScheduleCalendarTab} />`
break;
case ScheduleEventList.LIST:
tab = html`<${ScheduleEventListTab} />`
break;
}
return html`
<div style="margin: 32px">
<div class="folder">
<div class="tabs">
<div onclick=${() => this.tab.value = ScheduleEventList.CALENDAR} class="tab ${this.tab.value == ScheduleEventList.CALENDAR ? 'selected' : ''}">Calendar</div>
<div onclick=${() => this.tab.value = ScheduleEventList.LIST} class="tab ${this.tab.value == ScheduleEventList.LIST ? 'selected' : ''}">List</div>
<div class="hack"></div>
</div>
<div class="content">
${tab}
</div>
</div>
</div>
`
}//}}}
}
class ScheduleEventListTab extends Component {
constructor() {//{{{
super()
this.events = signal(null)
@ -995,7 +1030,11 @@ class ScheduleEventList extends Component {
if (this.events.value === null)
return
let events = this.events.value.map(evt => {
let events = this.events.value.sort((a, b) => {
if (a.Time < b.Time) return -1
if (a.Time > b.Time) return 1
return 0
}).map(evt => {
const dt = evt.Time.split('T')
const remind = () => {
if (evt.RemindMinutes > 0)
@ -1032,5 +1071,47 @@ class ScheduleEventList extends Component {
}//}}}
}
class ScheduleCalendarTab extends Component {
constructor() {//{{{
super()
}//}}}
componentDidMount() {
let calendarEl = document.getElementById('fullcalendar');
this.calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
events: this.events,
eventTimeFormat: {
hour12: false,
hour: '2-digit',
minute: '2-digit',
},
firstDay: 1,
aspectRatio: 2.5,
});
this.calendar.render();
}
render() {
return html`<div id="fullcalendar"></div>`
}
events(info, successCallback, failureCallback) {
const req = {
StartDate: info.startStr,
EndDate: info.endStr,
}
_app.current.request('/schedule/list', req)
.then(data => {
const fullcalendarEvents = data.ScheduleEvents.map(sch => {
return {
title: sch.Description,
start: sch.Time,
url: `/?node=${sch.Node.ID}`,
}
})
successCallback(fullcalendarEvents)
})
.catch(err=>failureCallback(err))
}
}
// vim: foldmethod=marker

View File

@ -8,6 +8,10 @@ html {
box-sizing: inherit;
}
*,*:focus,*:hover{
outline:none;
}
[onClick] {
cursor: pointer;
}
@ -476,6 +480,7 @@ header {
}
.checklist-item {
transform: translate(0, 0);
display: grid;
grid-template-columns: repeat(3, min-content);
grid-gap: 0 8px;
@ -905,6 +910,69 @@ header {
}
}
#input-text {
border: 1px solid #000 !important;
padding: 16px;
width: 300px;
.label {
margin-bottom: 4px;
}
input[type=text] {
width: 100%;
padding: 4px;
}
.buttons {
display: grid;
grid-template-columns: 1fr 64px 64px;
grid-gap: 8px;
margin-top: 8px;
}
}
#fullcalendar {
margin: 32px;
color: #444;
}
.folder {
.tabs {
border-left: 1px solid #888;
display: flex;
.tab {
padding: 16px 32px;
border-top: 1px solid #888;
border-bottom: 1px solid #888;
border-right: 1px solid #888;
color: #444;
background: #eee;
cursor: pointer;
&.selected {
border-bottom: none;
background: #fff;
}
}
.hack {
border-bottom: 1px solid #888;
width: 100%;
}
}
.content {
padding-top: 1px;
border-left: 1px solid #888;
border-right: 1px solid #888;
border-bottom: 1px solid #888;
padding-bottom: 1px;
}
}
@media only screen and (max-width: 932px) {
#app.node {
.layout-crumbs();

View File

@ -1 +1 @@
v27
v29