Merge branch 'ulsklyc:main' into main

This commit is contained in:
Rafael Foster
2026-04-29 05:48:02 -03:00
committed by GitHub
19 changed files with 125 additions and 34 deletions
+4 -3
View File
@@ -24,6 +24,7 @@ jobs:
uses: anthropics/claude-code-action@v1 uses: anthropics/claude-code-action@v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' prompt: >
plugins: 'code-review@claude-code-plugins' Review this pull request for bugs, logic errors, security issues, and adherence to project
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' conventions. Focus only on high-confidence issues that genuinely matter. Skip style nitpicks.
Post your findings as inline review comments on the relevant lines.
+5
View File
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.31.2] - 2026-04-29
### Added
- Settings: edit button (pencil icon) on each ICS subscription row — opens a modal to update name, color, and shared visibility via the existing PATCH endpoint (#100)
## [0.31.1] - 2026-04-29 ## [0.31.1] - 2026-04-29
### Fixed ### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "oikos", "name": "oikos",
"version": "0.31.1", "version": "0.31.2",
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.", "description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
"main": "server/index.js", "main": "server/index.js",
"type": "module", "type": "module",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "خاص", "private": "خاص",
"shared": "مشترك" "shared": "مشترك"
} },
"updatedToast": "تم تحديث الاشتراك."
}, },
"memberPhoneLabel": "رقم الهاتف (اختياري)", "memberPhoneLabel": "رقم الهاتف (اختياري)",
"memberEmailLabel": "البريد الإلكتروني (اختياري)", "memberEmailLabel": "البريد الإلكتروني (اختياري)",
+2 -1
View File
@@ -757,7 +757,8 @@
"empty": "Noch keine Abonnements.", "empty": "Noch keine Abonnements.",
"addedToast": "Abonnement hinzugefügt.", "addedToast": "Abonnement hinzugefügt.",
"syncedToast": "Abonnement synchronisiert.", "syncedToast": "Abonnement synchronisiert.",
"deletedToast": "Abonnement gelöscht." "deletedToast": "Abonnement gelöscht.",
"updatedToast": "Abonnement aktualisiert."
}, },
"memberPhoneLabel": "Telefonnummer (optional)", "memberPhoneLabel": "Telefonnummer (optional)",
"memberEmailLabel": "E-Mail (optional)", "memberEmailLabel": "E-Mail (optional)",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "Ιδιωτικό", "private": "Ιδιωτικό",
"shared": "Κοινόχρηστο" "shared": "Κοινόχρηστο"
} },
"updatedToast": "Η συνδρομή ενημερώθηκε."
}, },
"memberPhoneLabel": "Αριθμός τηλεφώνου (προαιρετικό)", "memberPhoneLabel": "Αριθμός τηλεφώνου (προαιρετικό)",
"memberEmailLabel": "Email (προαιρετικό)", "memberEmailLabel": "Email (προαιρετικό)",
+2 -1
View File
@@ -735,7 +735,8 @@
"badges": { "badges": {
"private": "Private", "private": "Private",
"shared": "Shared" "shared": "Shared"
} },
"updatedToast": "Subscription updated."
}, },
"memberPhoneLabel": "Phone number (optional)", "memberPhoneLabel": "Phone number (optional)",
"memberEmailLabel": "Email (optional)", "memberEmailLabel": "Email (optional)",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "Privado", "private": "Privado",
"shared": "Compartido" "shared": "Compartido"
} },
"updatedToast": "Suscripción actualizada."
}, },
"memberPhoneLabel": "Número de teléfono (opcional)", "memberPhoneLabel": "Número de teléfono (opcional)",
"memberEmailLabel": "Correo electrónico (opcional)", "memberEmailLabel": "Correo electrónico (opcional)",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "Privé", "private": "Privé",
"shared": "Partagé" "shared": "Partagé"
} },
"updatedToast": "Abonnement mis à jour."
}, },
"memberPhoneLabel": "Numéro de téléphone (facultatif)", "memberPhoneLabel": "Numéro de téléphone (facultatif)",
"memberEmailLabel": "E-mail (facultatif)", "memberEmailLabel": "E-mail (facultatif)",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "निजी", "private": "निजी",
"shared": "साझा" "shared": "साझा"
} },
"updatedToast": "सदस्यता अपडेट की गई।"
}, },
"memberPhoneLabel": "फ़ोन नंबर (वैकल्पिक)", "memberPhoneLabel": "फ़ोन नंबर (वैकल्पिक)",
"memberEmailLabel": "ईमेल (वैकल्पिक)", "memberEmailLabel": "ईमेल (वैकल्पिक)",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "Privato", "private": "Privato",
"shared": "Condiviso" "shared": "Condiviso"
} },
"updatedToast": "Abbonamento aggiornato."
}, },
"memberPhoneLabel": "Numero di telefono (opzionale)", "memberPhoneLabel": "Numero di telefono (opzionale)",
"memberEmailLabel": "E-mail (opzionale)", "memberEmailLabel": "E-mail (opzionale)",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "プライベート", "private": "プライベート",
"shared": "共有" "shared": "共有"
} },
"updatedToast": "サブスクリプションが更新されました。"
}, },
"memberPhoneLabel": "電話番号(任意)", "memberPhoneLabel": "電話番号(任意)",
"memberEmailLabel": "メールアドレス(任意)", "memberEmailLabel": "メールアドレス(任意)",
+2 -1
View File
@@ -735,7 +735,8 @@
"badges": { "badges": {
"private": "Privado", "private": "Privado",
"shared": "Partilhado" "shared": "Partilhado"
} },
"updatedToast": "Subscrição atualizada."
}, },
"memberPhoneLabel": "Telefone (opcional)", "memberPhoneLabel": "Telefone (opcional)",
"memberEmailLabel": "E-mail (opcional)", "memberEmailLabel": "E-mail (opcional)",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "Личное", "private": "Личное",
"shared": "Общее" "shared": "Общее"
} },
"updatedToast": "Подписка обновлена."
}, },
"memberPhoneLabel": "Номер телефона (необязательно)", "memberPhoneLabel": "Номер телефона (необязательно)",
"memberEmailLabel": "Электронная почта (необязательно)", "memberEmailLabel": "Электронная почта (необязательно)",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "Privat", "private": "Privat",
"shared": "Delad" "shared": "Delad"
} },
"updatedToast": "Prenumeration uppdaterad."
}, },
"memberPhoneLabel": "Telefonnummer (valfritt)", "memberPhoneLabel": "Telefonnummer (valfritt)",
"memberEmailLabel": "E-post (valfritt)", "memberEmailLabel": "E-post (valfritt)",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "Özel", "private": "Özel",
"shared": "Paylaşımlı" "shared": "Paylaşımlı"
} },
"updatedToast": "Abonelik güncellendi."
}, },
"memberPhoneLabel": "Telefon numarası (isteğe bağlı)", "memberPhoneLabel": "Telefon numarası (isteğe bağlı)",
"memberEmailLabel": "E-posta (isteğe bağlı)", "memberEmailLabel": "E-posta (isteğe bağlı)",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "Приватне", "private": "Приватне",
"shared": "Спільне" "shared": "Спільне"
} },
"updatedToast": "Підписку оновлено."
}, },
"memberPhoneLabel": "Номер телефону (необов'язково)", "memberPhoneLabel": "Номер телефону (необов'язково)",
"memberEmailLabel": "Електронна пошта (необов'язково)", "memberEmailLabel": "Електронна пошта (необов'язково)",
+2 -1
View File
@@ -732,7 +732,8 @@
"badges": { "badges": {
"private": "私人", "private": "私人",
"shared": "共享" "shared": "共享"
} },
"updatedToast": "订阅已更新。"
}, },
"memberPhoneLabel": "电话号码(可选)", "memberPhoneLabel": "电话号码(可选)",
"memberEmailLabel": "电子邮件(可选)", "memberEmailLabel": "电子邮件(可选)",
+70
View File
@@ -1644,6 +1644,19 @@ function renderIcsList(container, subs, user) {
syncBtn.appendChild(syncIcon); syncBtn.appendChild(syncIcon);
actions.appendChild(syncBtn); actions.appendChild(syncBtn);
const editBtn = document.createElement('button');
editBtn.className = 'btn btn--icon btn--ghost';
editBtn.title = t('settings.ics.actions.edit');
editBtn.setAttribute('aria-label', t('settings.ics.actions.edit'));
editBtn.dataset.action = 'ics-edit';
editBtn.dataset.id = sub.id;
const editIcon = document.createElement('i');
editIcon.setAttribute('data-lucide', 'pencil');
editIcon.style.cssText = 'width:14px;height:14px';
editIcon.setAttribute('aria-hidden', 'true');
editBtn.appendChild(editIcon);
actions.appendChild(editBtn);
const delBtn = document.createElement('button'); const delBtn = document.createElement('button');
delBtn.className = 'btn btn--icon btn--danger-outline'; delBtn.className = 'btn btn--icon btn--danger-outline';
delBtn.title = t('settings.ics.actions.delete'); delBtn.title = t('settings.ics.actions.delete');
@@ -1756,6 +1769,63 @@ function bindIcsEvents(container, user, initialSubs) {
} }
} }
if (action === 'ics-edit') {
const sub = subs.find((s) => s.id === id);
if (!sub) return;
openModal({
title: t('settings.ics.actions.edit'),
size: 'sm',
content: `
<form id="ics-edit-form" class="settings-form">
<div class="form-group">
<label class="form-label" for="ics-edit-name">${t('settings.ics.form.name')}</label>
<input class="form-input" type="text" id="ics-edit-name" value="${esc(sub.name)}" required maxlength="100" />
</div>
<div class="settings-name-color-row">
<div class="form-group settings-color-field">
<label class="form-label" for="ics-edit-color">${t('settings.ics.form.color')}</label>
<input class="settings-color-button" type="color" id="ics-edit-color" value="${esc(sub.color || '#3b82f6')}" />
</div>
<div class="form-group" style="display:flex;align-items:center;gap:var(--space-2);padding-top:var(--space-5)">
<input type="checkbox" id="ics-edit-shared" ${sub.shared ? 'checked' : ''} />
<label class="form-label" for="ics-edit-shared" style="margin:0">${t('settings.ics.form.shared')}</label>
</div>
</div>
<div id="ics-edit-error" class="form-error" hidden></div>
<div class="settings-form-actions">
<button type="button" class="btn btn--secondary" id="ics-edit-cancel">${t('common.cancel')}</button>
<button type="submit" class="btn btn--primary">${t('settings.ics.actions.save')}</button>
</div>
</form>
`,
onSave(panel) {
panel.querySelector('#ics-edit-cancel')?.addEventListener('click', () => closeModal());
panel.querySelector('#ics-edit-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = panel.querySelector('[type=submit]');
const errEl = panel.querySelector('#ics-edit-error');
const name = panel.querySelector('#ics-edit-name').value.trim();
const color = panel.querySelector('#ics-edit-color').value;
const shared = panel.querySelector('#ics-edit-shared').checked ? 1 : 0;
errEl.hidden = true;
submitBtn.disabled = true;
try {
const res = await api.patch(`/calendar/subscriptions/${id}`, { name, color, shared });
const idx = subs.findIndex((s) => s.id === id);
if (idx >= 0) subs[idx] = res.data;
renderIcsList(container, subs, user);
window.oikos?.showToast(t('settings.ics.updatedToast'), 'success');
closeModal({ force: true });
} catch (err) {
errEl.textContent = err.message ?? t('common.errorGeneric');
errEl.hidden = false;
submitBtn.disabled = false;
}
});
},
});
}
if (action === 'ics-delete') { if (action === 'ics-delete') {
const name = target.dataset.name; const name = target.dataset.name;
if (!await confirmModal(t('settings.ics.confirm_delete'), { danger: true, confirmLabel: t('common.delete') })) return; if (!await confirmModal(t('settings.ics.confirm_delete'), { danger: true, confirmLabel: t('common.delete') })) return;