From 207fdbc408f974c1b57e101f742bdfbcc47e8600 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:15:51 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20issues=20et=20PR=20suivies=20par=20?= =?UTF-8?q?projet=20(seulement=20les=20miennes=20sur=20les=20d=C3=A9p?= =?UTF-8?q?=C3=B4ts=20des=20autres),=20cr=C3=A9er=20une=20t=C3=A2che=20dep?= =?UTF-8?q?uis=20Mon=20GitHub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SYMcy2ff4fam7pKKMjdkGa --- CLAUDE.md | 2 +- README.en.md | 4 ++-- README.md | 4 ++-- server/src/db.js | 8 ++++++- server/src/insights.js | 3 ++- server/src/routes/integrations.js | 2 +- server/src/routes/overview.js | 7 +++--- server/src/routes/projects.js | 16 +++++++++----- server/src/sync/index.js | 8 +++++-- server/src/visibility.js | 26 +++++++++++++++++++++++ server/test/forges.test.js | 19 +++++++++++++++++ web/src/components/GithubPanel.jsx | 12 +++++++---- web/src/components/ProjectForm.jsx | 14 +++++++++++- web/src/i18n/en.js | 13 ++++++++++++ web/src/pages/MyGithub.jsx | 34 +++++++++++++++++++++++++++--- 15 files changed, 146 insertions(+), 26 deletions(-) create mode 100644 server/src/visibility.js diff --git a/CLAUDE.md b/CLAUDE.md index df36c00..4f81859 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ - node:sqlite refuse `undefined` et les booléens : passer `null` / `0|1`. - Synchro (`server/src/sync/`) : toute nouveauté passe par `pushInbox` avec une `external_key` unique ; la première synchro d'une source est « baselinée » (marquée lue) via la table `settings`. - `pushInbox` remplit aussi `fresh` (entrées nouvelles et non lues) : c'est ce qui part sur ntfy en fin de `syncProject` (`sync/notify.js`). La CI ne génère une entrée qu'aux changements d'état (vert → rouge, rouge → vert). -- Forges (`server/src/sync/forges/`) : un adaptateur par forge (github, gitlab, gitea — Forgejo = gitea) avec la même interface (`fetchRepo`, `fetchCi`, `latestRelease`, `createIssue`, `listOwnerRepos`). Toujours passer par `forgeFor(db, project)` qui résout le token (comptes `config:forges` puis variables d'env). `whoami()` (compte du token) et `fetchMentions` (GitHub) remplissent `github_items.mine` (author, assignee, review, mention) pour les filtres « Me concernant » et la page « Mon GitHub » (`/api/my-github`) ; `fetchPrState` (GitHub) remplit `github_items.pr_state` pour ces PR, et les transitions sur les PR de l'utilisateur deviennent des entrées `pr_status`. Un bug venu du forum fermé par la synchro crée une entrée `forum_reply` (`items.forum_replied_at` une fois répondu). Sujets suivis : table `forum_watches` (`fetchTopic`, entrées `forum_followed`). CI GitHub : seuls les runs `push` / `merge_group` comptent, GitLab ignore les pipelines `schedule`. Checklist de release : `milestones.checklist`, modèle dans `config:release_checklist`. Noms historiques : la table `github_items` et la colonne `projects.github` contiennent les données de toutes les forges. +- Forges (`server/src/sync/forges/`) : un adaptateur par forge (github, gitlab, gitea — Forgejo = gitea) avec la même interface (`fetchRepo`, `fetchCi`, `latestRelease`, `createIssue`, `listOwnerRepos`). Toujours passer par `forgeFor(db, project)` qui résout le token (comptes `config:forges` puis variables d'env). `whoami()` (compte du token) et `fetchMentions` (GitHub) remplissent `github_items.mine` (author, assignee, review, mention) pour les filtres « Me concernant » et la page « Mon GitHub » (`/api/my-github`) ; `fetchPrState` (GitHub) remplit `github_items.pr_state` pour ces PR, et les transitions sur les PR de l'utilisateur deviennent des entrées `pr_status`. Un bug venu du forum fermé par la synchro crée une entrée `forum_reply` (`items.forum_replied_at` une fois répondu). Sujets suivis : table `forum_watches` (`fetchTopic`, entrées `forum_followed`). CI GitHub : seuls les runs `push` / `merge_group` comptent, GitLab ignore les pipelines `schedule`. Portée par projet : `projects.github_scope` (auto | all | mine) → `projects.mine_only` (recalculé par `refreshMineOnly` et à la synchro) ; toute requête qui compte ou liste les `github_items` passe par `VISIBLE_ITEM` (`server/src/visibility.js`). Checklist de release : `milestones.checklist`, modèle dans `config:release_checklist`. Noms historiques : la table `github_items` et la colonne `projects.github` contiennent les données de toutes les forges. - Source de version `repo` = releases du dépôt du projet (toute forge) ; `github` = autre dépôt GitHub. - Tâches planifiées (`server/src/jobs.js`, tick 60 s) : résumé hebdo ntfy (`insights.js`) et sauvegardes `VACUUM INTO` (`backup.js`, `BACKUP_DIR`). Le flux `/api/calendar.ics?token=` échappe à `APP_PASSWORD` (clé dans `config:calendar`). Statistiques : `fetchActivity` des forges, agrégées par `stats.js`. - Pièces jointes (`server/src/uploads.js`) : fichiers dans `UPLOADS_DIR` (défaut `DATA_PATH/uploads`), référencés uniquement depuis le Markdown (`/api/uploads/`), sans table ; les orphelins sont supprimés au moment de la sauvegarde. Côté front, `MarkdownInput` remplace `Input.TextArea` pour tout champ Markdown. Vue « Aujourd'hui » : colonne `items.focus_date` (`server/src/today.js`). diff --git a/README.en.md b/README.en.md index 4fbf716..1ae4f6d 100644 --- a/README.en.md +++ b/README.en.md @@ -28,8 +28,8 @@ Self-hosted (Docker), single-user, all data in one SQLite file. | **Projects grouped by category** | Freelance, Personal, Organisation (e.g. Gladys), Other — editable categories with colours. Status (idea, active, maintenance, paused, archived), client, tags, links, pinning. | | **GitHub, GitLab, Gitea / Forgejo** | Each project points to a repository on github.com, gitlab.com or a self-hosted instance (GitLab, Gitea, Forgejo). One token per instance in **Settings → Forges and tokens**. | | **Bulk repository import** | Lists the repositories of a user, organisation or group (filter, forks, archived) and creates the projects in one go, with category, status and release tracking. Repositories already tracked are skipped. | -| **Repository tracking** | Open issues and PRs / MRs, stars, last activity. "For me" filters (opened by you, assigned, review requested, mentioned — based on the token's account), by label, author or title, remembered per project: handy on a very active repository like Gladys. **New** ones land in the inbox; a "Track" button adds them to your bugs & tasks. | -| **My GitHub** | One page gathering, across all your projects, the reviews requested from you, your PRs, what is assigned to you, your mentions and your issues. Each PR shows its state: approved, changes requested, conflict with the main branch, CI. On your PRs, an approval, a change request, a conflict or a red CI lands in the inbox and on ntfy. | +| **Repository tracking** | Open issues and PRs / MRs, stars, last activity. "For me" filters (opened by you, assigned, review requested, mentioned — based on the token's account), by label, author or title, remembered per project: handy on a very active repository like Gladys. Per-project "Issues and PRs followed" setting: Auto (default) follows everything on your repositories and only what concerns you on other people's (counters, dashboard, stale list, notifications). **New** ones land in the inbox; a "Track" button adds them to your bugs & tasks. | +| **My GitHub** | One page gathering, across all your projects, the reviews requested from you, your PRs, what is assigned to you, your mentions and your issues. Each PR shows its state: approved, changes requested, conflict with the main branch, CI. One click turns it into a task with an explicit title ("Resolve the conflict: #3028…"). On your PRs, an approval, a change request, a conflict or a red CI lands in the inbox and on ntfy. | | **Create an issue from a bug** | A bug reported on a forum becomes an issue on the repository (title, description, forum link, target version, label). Linked both ways: when the issue is closed, the bug is marked as done. | | **CI status** | GitHub Actions (latest run of each workflow), GitLab pipelines, Gitea/Forgejo commit statuses (Actions, Woodpecker, Drone) on the default branch: green/red badge (only runs triggered by the code count: scheduled jobs and manually started workflows are ignored), dashboard alert, notification when CI breaks and when it is green again. | | **Personal GitHub notifications** | Review requests, mentions, assignments, replies on your issues/PRs (e.g. to GladysAssistant/Gladys), attached to the matching tracked project. Requires a classic token with the `notifications` scope. | diff --git a/README.md b/README.md index b9de8f1..5363140 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ Auto-hébergé (Docker), mono-utilisateur, données dans un seul fichier SQLite. | **Projets classés par catégories** | Freelance, Perso, Organisation (Gladys…), Autre — catégories modifiables avec couleur. Statut (idée, actif, maintenance, pause, archivé), client, tags, liens, épinglage. | | **GitHub, GitLab, Gitea / Forgejo** | Chaque projet pointe vers un dépôt sur github.com, gitlab.com ou une instance auto-hébergée (GitLab, Gitea, Forgejo). Un token par instance dans **Paramètres → Forges et tokens**. | | **Import de dépôts en masse** | Liste les dépôts d'un utilisateur, d'une organisation ou d'un groupe (filtre, forks, archivés) et crée les projets d'un coup avec catégorie, statut et suivi des releases. Les dépôts déjà suivis sont ignorés. | -| **Suivi des dépôts** | Issues et PR / MR ouvertes, étoiles, dernière activité. Filtres « Me concernant » (ouvert par toi, assigné, review demandée, mentionné — d'après le compte du token), par label, auteur ou titre, mémorisés par projet : pratique sur un dépôt très actif comme Gladys. Les **nouvelles** arrivent dans la boîte de réception ; un bouton « Suivre » les ajoute aux bugs & tâches. | -| **Mon GitHub** | Une page qui réunit, sur tous tes projets, les reviews qu'on te demande, tes PR, ce qui t'est assigné, tes mentions et tes issues. Chaque PR affiche son état : approuvée, changements demandés, conflit avec la branche principale, CI. Sur tes PR, une approbation, une demande de changements, un conflit ou une CI rouge arrive dans la boîte de réception et sur ntfy. | +| **Suivi des dépôts** | Issues et PR / MR ouvertes, étoiles, dernière activité. Filtres « Me concernant » (ouvert par toi, assigné, review demandée, mentionné — d'après le compte du token), par label, auteur ou titre, mémorisés par projet : pratique sur un dépôt très actif comme Gladys. Réglage par projet « Issues et PR suivies » : en mode Auto (par défaut), tout est suivi sur tes dépôts, et seulement ce qui te concerne sur ceux des autres (compteurs, tableau de bord, « En attente », notifications). Les **nouvelles** arrivent dans la boîte de réception ; un bouton « Suivre » les ajoute aux bugs & tâches. | +| **Mon GitHub** | Une page qui réunit, sur tous tes projets, les reviews qu'on te demande, tes PR, ce qui t'est assigné, tes mentions et tes issues. Chaque PR affiche son état : approuvée, changements demandés, conflit avec la branche principale, CI. Un clic en fait une tâche au titre explicite (« Résoudre le conflit : #3028… »). Sur tes PR, une approbation, une demande de changements, un conflit ou une CI rouge arrive dans la boîte de réception et sur ntfy. | | **Créer une issue depuis un bug** | Un bug remonté par le forum devient une issue sur le dépôt (titre, description, lien du forum, version cible, label). Lien dans les deux sens : quand l'issue est fermée, le bug passe à « Terminé ». | | **État de la CI** | GitHub Actions (dernier run de chaque workflow), pipelines GitLab, statuts de commit Gitea/Forgejo (Actions, Woodpecker, Drone) sur la branche principale : pastille verte/rouge (seuls les runs déclenchés par le code comptent : les tâches planifiées et les workflows lancés à la main sont ignorés), alerte au tableau de bord, notification quand la CI casse puis repasse au vert. | | **Notifications GitHub perso** | Reviews demandées, mentions, assignations, réponses sur tes issues/PR (ex. vers GladysAssistant/Gladys), rattachées au projet suivi correspondant. Nécessite un token classique avec le scope `notifications`. | diff --git a/server/src/db.js b/server/src/db.js index 6d2a2e9..2e09097 100644 --- a/server/src/db.js +++ b/server/src/db.js @@ -191,6 +191,12 @@ const MIGRATIONS = [ -- Release checklist of a version: [{ label, done }]. ALTER TABLE milestones ADD COLUMN checklist TEXT; `, + ` + -- Which issues / PRs of the repository are followed: auto | all | mine. + ALTER TABLE projects ADD COLUMN github_scope TEXT NOT NULL DEFAULT 'auto'; + -- Effective value (1 = only those that concern the user), kept up to date. + ALTER TABLE projects ADD COLUMN mine_only INTEGER NOT NULL DEFAULT 0; + `, ] export function openDb(file = join(DATA_PATH, 'projectory.db')) { @@ -223,7 +229,7 @@ export function decode(row) { try { out[col] = JSON.parse(out[col]) } catch { /* keep raw */ } } } - for (const col of ['pinned', 'draft', 'notify']) if (col in out) out[col] = !!out[col] + for (const col of ['pinned', 'draft', 'notify', 'mine_only']) if (col in out) out[col] = !!out[col] return out } diff --git a/server/src/insights.js b/server/src/insights.js index 37f38b8..2ed7a03 100644 --- a/server/src/insights.js +++ b/server/src/insights.js @@ -1,3 +1,4 @@ +import { VISIBLE_ITEM } from './visibility.js' import { getConfig } from './db.js' import { tr } from './i18n.js' import { compareVersions } from './sync/http.js' @@ -17,7 +18,7 @@ export function findStale(db, now = new Date()) { const base = `SELECT g.id, g.project_id, g.type, g.number, g.title, g.url, g.author, g.comments, g.created_at, g.updated_at, p.name AS project_name, p.forge FROM github_items g JOIN projects p ON p.id = g.project_id - WHERE p.status != 'archived'` + WHERE p.status != 'archived' AND ${VISIBLE_ITEM}` const issues = db.prepare(`${base} AND g.type = 'issue' AND g.comments = 0 AND g.created_at <= ?`).all(daysAgo(issue_days, now)) .map((r) => ({ ...r, reason: 'no_reply', age_days: ageDays(r.created_at, now) })) const prs = db.prepare(`${base} AND g.type = 'pr' AND g.draft = 0 AND g.updated_at <= ?`).all(daysAgo(pr_days, now)) diff --git a/server/src/routes/integrations.js b/server/src/routes/integrations.js index c57b285..6cdec10 100644 --- a/server/src/routes/integrations.js +++ b/server/src/routes/integrations.js @@ -140,7 +140,7 @@ export default async function integrationRoutes(app, { db }) { project_id: g.project_id, kind: req.body?.kind || (g.type === 'pr' ? 'task' : 'bug'), priority: req.body?.priority || 'normal', - title: g.title, + title: String(req.body?.title || '').trim() || g.title, source: 'github', issue_number: g.type === 'issue' ? g.number : null, issue_url: g.url, diff --git a/server/src/routes/overview.js b/server/src/routes/overview.js index e7ae449..459e957 100644 --- a/server/src/routes/overview.js +++ b/server/src/routes/overview.js @@ -5,6 +5,7 @@ import { forgeContext } from '../sync/forges/index.js' import { tr } from '../i18n.js' import { findStale } from '../insights.js' import { todayView } from '../today.js' +import { VISIBLE_ITEM } from '../visibility.js' const REPO_SOURCES = "'github', 'gitlab', 'gitea'" const INBOX_SQL = `SELECT x.*, p.name AS project_name FROM inbox x LEFT JOIN projects p ON p.id = x.project_id` @@ -59,8 +60,8 @@ export default async function overviewRoutes(app, { db }) { stats: { projects: projects.length, active: projects.filter((p) => p.status === 'active').length, - open_issues: count("SELECT count(*) n FROM github_items WHERE type = 'issue'"), - open_prs: count("SELECT count(*) n FROM github_items WHERE type = 'pr'"), + open_issues: count(`SELECT count(*) n FROM github_items g JOIN projects p ON p.id = g.project_id WHERE g.type = 'issue' AND p.status != 'archived' AND ${VISIBLE_ITEM}`), + open_prs: count(`SELECT count(*) n FROM github_items g JOIN projects p ON p.id = g.project_id WHERE g.type = 'pr' AND p.status != 'archived' AND ${VISIBLE_ITEM}`), ci_failing: projects.filter((p) => p.ci === 'failure').length, stale: findStale(db).length, open_bugs: count("SELECT count(*) n FROM items WHERE kind = 'bug' AND status IN ('todo', 'in_progress')"), @@ -80,7 +81,7 @@ export default async function overviewRoutes(app, { db }) { stale: findStale(db).slice(0, 8), recent_prs: db.prepare( `SELECT g.*, p.name AS project_name FROM github_items g JOIN projects p ON p.id = g.project_id - WHERE g.type = 'pr' ORDER BY g.updated_at DESC LIMIT 8` + WHERE g.type = 'pr' AND ${VISIBLE_ITEM} ORDER BY g.updated_at DESC LIMIT 8` ).all().map(decode), sync: syncState, } diff --git a/server/src/routes/projects.js b/server/src/routes/projects.js index 05ed2cd..64afc02 100644 --- a/server/src/routes/projects.js +++ b/server/src/routes/projects.js @@ -3,18 +3,22 @@ import { compareVersions } from '../sync/http.js' import { normalizeRepo } from '../sync/forges/index.js' import { syncProject } from '../sync/index.js' import { tr } from '../i18n.js' +import { VISIBLE_ITEM, refreshMineOnly } from '../visibility.js' const FIELDS = [ 'name', 'description', 'category_id', 'status', 'tags', 'client', 'repo', 'homepage', 'links', - 'deployed_version', 'version_sources', 'forum_sources', 'pinned', 'notify', 'forge', 'forge_url', + 'deployed_version', 'version_sources', 'forum_sources', 'pinned', 'notify', 'forge', 'forge_url', 'github_scope', ] +// Item of Bugs & tasks already created from this issue / PR, if any. +const TRACKED = '(SELECT i.id FROM items i WHERE i.project_id = g.project_id AND i.issue_url = g.url LIMIT 1) AS item_id' + const LIST_SQL = ` SELECT p.*, c.name AS category_name, c.color AS category_color, (SELECT count(*) FROM items i WHERE i.project_id = p.id AND i.status IN ('todo', 'in_progress')) AS open_items, (SELECT count(*) FROM items i WHERE i.project_id = p.id AND i.status IN ('todo', 'in_progress') AND i.kind = 'bug') AS open_bugs, - (SELECT count(*) FROM github_items g WHERE g.project_id = p.id AND g.type = 'issue') AS open_issues, - (SELECT count(*) FROM github_items g WHERE g.project_id = p.id AND g.type = 'pr') AS open_prs, + (SELECT count(*) FROM github_items g WHERE g.project_id = p.id AND g.type = 'issue' AND ${VISIBLE_ITEM}) AS open_issues, + (SELECT count(*) FROM github_items g WHERE g.project_id = p.id AND g.type = 'pr' AND ${VISIBLE_ITEM}) AS open_prs, (SELECT count(*) FROM inbox x WHERE x.project_id = p.id AND x.read_at IS NULL) AS unread FROM projects p LEFT JOIN categories c ON c.id = p.category_id ` @@ -64,12 +68,14 @@ export default async function projectRoutes(app, { db }) { app.post('/projects', async (req, reply) => { if (!req.body?.name?.trim()) return reply.code(400).send({ error: tr('Le nom est obligatoire') }) const id = insertRow(db, 'projects', clean(req.body), FIELDS) + refreshMineOnly(db, id) return reply.code(201).send(listProjects(db, { where: ['p.id = ?'], params: [id] })[0]) }) app.put('/projects/:id', async (req, reply) => { const id = Number(req.params.id) if (!updateRow(db, 'projects', id, clean(req.body || {}), FIELDS)) return reply.code(404).send({ error: tr('Projet introuvable') }) + refreshMineOnly(db, id) return listProjects(db, { where: ['p.id = ?'], params: [id] })[0] }) @@ -86,7 +92,7 @@ export default async function projectRoutes(app, { db }) { // Open issues / PRs that concern the user, across every project. app.get('/my-github', async () => db.prepare( - `SELECT g.*, p.name AS project_name, p.forge FROM github_items g JOIN projects p ON p.id = g.project_id + `SELECT g.*, p.name AS project_name, p.forge, ${TRACKED} FROM github_items g JOIN projects p ON p.id = g.project_id WHERE g.mine != '[]' AND p.status != 'archived' ORDER BY g.updated_at DESC` ).all().map(decode)) @@ -97,7 +103,7 @@ export default async function projectRoutes(app, { db }) { if (project_id) { where.push('g.project_id = ?'); params.push(Number(project_id)) } if (type) { where.push('g.type = ?'); params.push(type) } return db.prepare( - `SELECT g.*, p.name AS project_name FROM github_items g JOIN projects p ON p.id = g.project_id + `SELECT g.*, p.name AS project_name, ${TRACKED} FROM github_items g JOIN projects p ON p.id = g.project_id WHERE ${where.join(' AND ')} ORDER BY g.updated_at DESC` ).all(...params).map(decode) }) diff --git a/server/src/sync/index.js b/server/src/sync/index.js index 19b07ab..08b8403 100644 --- a/server/src/sync/index.js +++ b/server/src/sync/index.js @@ -1,6 +1,7 @@ import { decode, getConfig } from '../db.js' import { failedRun, forgeFor, normalizeRepo } from './forges/index.js' import { involvement } from './forges/common.js' +import { computeMineOnly } from '../visibility.js' import { fetchForum, fetchTopic, forumSourceKey, latestVersion } from './sources.js' import { notifyProject } from './notify.js' import { tr } from '../i18n.js' @@ -53,6 +54,9 @@ async function syncForge(db, project, fresh, errors) { const login = await forge.whoami?.().catch(() => null) const mentioned = login && forge.fetchMentions ? await forge.fetchMentions(repo, login).catch(() => new Set()) : new Set() const mineOf = new Map(items.map((it) => [it, involvement(it, login, mentioned)])) + // Someone else's busy repository: only the user's own issues / PRs count. + info.viewer = login || project.github?.viewer || null + const mineOnly = computeMineOnly({ ...project, github: info }) // Review / CI / conflict state of the PRs that concern the user (capped: one // request set per PR). Fetched before the transaction, never fatal. const prStates = new Map() @@ -100,7 +104,7 @@ async function syncForge(db, project, fresh, errors) { if (mineOf.get(it).includes('author')) { pushPrTransitions(db, project, baseKey, it, previousStates.get(it.number), prStates.get(it.number), { asRead, fresh }) } - if (!existing.has(`${it.type}#${it.number}`)) { + if (!existing.has(`${it.type}#${it.number}`) && (!mineOnly || mineOf.get(it).length)) { pushInbox(db, { project_id: project.id, source, @@ -124,7 +128,7 @@ async function syncForge(db, project, fresh, errors) { } } closeLinkedItems(db, project, items, baseKey, { asRead, fresh }) - db.prepare('UPDATE projects SET github = ? WHERE id = ?').run(JSON.stringify(info), project.id) + db.prepare('UPDATE projects SET github = ?, mine_only = ? WHERE id = ?').run(JSON.stringify(info), mineOnly, project.id) markBaselined(db, baseKey) db.exec('COMMIT') } catch (err) { diff --git a/server/src/visibility.js b/server/src/visibility.js new file mode 100644 index 0000000..edc9736 --- /dev/null +++ b/server/src/visibility.js @@ -0,0 +1,26 @@ +import { normalizeRepo } from './sync/forges/index.js' + +// SQL condition on github_items g joined to projects p: on a project limited to +// the user's own issues / PRs, only those that concern them are counted. +export const VISIBLE_ITEM = "(p.mine_only = 0 OR g.mine != '[]')" + +// "auto": everything on the user's own repositories, only their own items on +// someone else's (e.g. GladysAssistant/Gladys); "all" / "mine" force it. +export function computeMineOnly(project) { + const scope = project.github_scope || 'auto' + if (scope === 'all') return 0 + if (scope === 'mine') return 1 + const viewer = project.github?.viewer + const owner = normalizeRepo(project.repo || '').split('/')[0] + return viewer && owner && owner.toLowerCase() !== viewer.toLowerCase() ? 1 : 0 +} + +export function refreshMineOnly(db, projectId) { + const row = db.prepare('SELECT repo, github, github_scope FROM projects WHERE id = ?').get(projectId) + if (!row) return 0 + let github = null + try { github = JSON.parse(row.github || 'null') } catch { /* keep null */ } + const value = computeMineOnly({ ...row, github }) + db.prepare('UPDATE projects SET mine_only = ? WHERE id = ?').run(value, projectId) + return value +} diff --git a/server/test/forges.test.js b/server/test/forges.test.js index b4d3ca2..9d9abfe 100644 --- a/server/test/forges.test.js +++ b/server/test/forges.test.js @@ -182,6 +182,25 @@ test('GitHub: flags the issues and PRs that concern the token owner', async () = const mine = Object.fromEntries(items.map((it) => [it.number, it.mine])) assert.deepEqual(mine, { 1: ['author'], 2: ['assignee'], 3: ['review'], 4: [], 5: ['mention'], 6: [], 7: ['author'] }) assert.deepEqual(items.find((it) => it.number === 2).assignees, ['will']) + + // org/big belongs to someone else: by default only the user's items are counted. + let project = (await call('GET', `/api/projects?q=Big`)).body.find((x) => x.id === p.id) + assert.equal(project.mine_only, true) + assert.deepEqual([project.open_issues, project.open_prs], [3, 2]) + const dash = (await call('GET', '/api/dashboard')).body + assert.ok(!dash.recent_prs.some((r) => r.project_id === p.id && r.number === 4), 'other people\'s PRs stay out of the dashboard') + project = (await call('PUT', `/api/projects/${p.id}`, { github_scope: 'all' })).body + assert.deepEqual([project.mine_only, project.open_issues, project.open_prs], [false, 4, 3]) + project = (await call('PUT', `/api/projects/${p.id}`, { github_scope: 'auto' })).body + assert.equal(project.mine_only, true) + + // A PR in conflict becomes a task with a custom title; tracking it again returns the same item. + const pr7 = items.find((it) => it.number === 7) + const { body: task } = await call('POST', `/api/github-items/${pr7.id}/track`, { title: 'Résoudre le conflit : #7', priority: 'high' }) + assert.deepEqual([task.title, task.kind, task.priority], ['Résoudre le conflit : #7', 'task', 'high']) + assert.equal((await call('POST', `/api/github-items/${pr7.id}/track`, {})).body.id, task.id) + const mineRow = (await call('GET', '/api/my-github')).body.find((it) => it.id === pr7.id) + assert.equal(mineRow.item_id, task.id) } finally { delete process.env.GITHUB_TOKEN } diff --git a/web/src/components/GithubPanel.jsx b/web/src/components/GithubPanel.jsx index 6fd172f..e9321bd 100644 --- a/web/src/components/GithubPanel.jsx +++ b/web/src/components/GithubPanel.jsx @@ -19,8 +19,10 @@ const MINE = { } const DEFAULT_FILTERS = { scope: 'all', labels: [], author: null, q: '' } -function readFilters(key) { - try { return { ...DEFAULT_FILTERS, ...JSON.parse(localStorage.getItem(key) || '{}') } } catch { return DEFAULT_FILTERS } +// Projects limited to the user's items open on "For me" until another view is chosen. +function readFilters(key, mineOnly) { + const defaults = { ...DEFAULT_FILTERS, scope: mineOnly ? 'mine' : 'all' } + try { return { ...defaults, ...JSON.parse(localStorage.getItem(key) || '{}') } } catch { return defaults } } // Open issues & PRs / MRs mirrored by the last sync. @@ -29,7 +31,7 @@ export default function GithubPanel({ projectId, project }) { const [type, setType] = useState('issue') // Filters are remembered per project: on a busy repository they are the default view. const storageKey = `projectory:gh-filters:${projectId || 'all'}` - const [filters, setFilters] = useState(() => readFilters(storageKey)) + const [filters, setFilters] = useState(() => readFilters(storageKey, project?.mine_only)) useEffect(() => { try { localStorage.setItem(storageKey, JSON.stringify({ ...filters, q: '' })) } catch { /* storage unavailable */ } }, [storageKey, filters]) @@ -115,7 +117,9 @@ export default function GithubPanel({ projectId, project }) { {it.comments} : null, - + - {t('ou colle / glisse une capture d’écran ici')} + {!preview && {t('ou colle / glisse une capture d’écran ici')}}