Multi-user sharing, groups, and in-app notifications
Backend: - Alembic migration 0025: groups, group_memberships, project_shares, note_shares, notifications tables - models: Group, GroupMembership, ProjectShare, NoteShare, Notification - services/access.py: permission resolution (viewer/editor/admin/owner) - services/groups.py + routes/groups.py: full group CRUD + membership - services/sharing.py + routes/shares.py: project/note sharing API - services/notifications.py: in-app notification create/list/mark-read - routes/in_app_notifications.py: GET/POST notification endpoints - routes/users.py: user search endpoint - services/projects.py + services/notes.py: *_for_user variants - routes updated to use *_for_user on get/list; adds permission field - app.py: register all new blueprints Frontend: - api/client.ts: ShareEntry, GroupEntry, NotificationEntry types + helpers - stores/notifications.ts: Pinia store with polling - NotificationBell.vue: bell icon + badge + dropdown toggle + 60s poll - NotificationsPanel.vue: unread notification list with mark-all-read - ShareDialog.vue: teleport modal for sharing projects/notes with users/groups - SharedWithMeView.vue: /shared route listing shared projects and notes - AppHeader: NotificationBell, Shared nav link - ProjectView, NoteViewerView, TaskViewerView: Share button + ShareDialog - SettingsView: Groups admin tab with create/delete groups + member mgmt - router: /shared route Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { apiGet, apiPatch, apiDelete, apiPost } from "@/api/client";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import { relativeTime } from "@/composables/useRelativeTime";
|
||||
import ShareDialog from "@/components/ShareDialog.vue";
|
||||
|
||||
interface Milestone {
|
||||
id: number;
|
||||
@@ -267,6 +268,7 @@ async function saveProject() {
|
||||
}
|
||||
|
||||
const showDeleteConfirm = ref(false);
|
||||
const showShare = ref(false);
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!project.value) return;
|
||||
@@ -294,6 +296,7 @@ async function confirmDelete() {
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
|
||||
Workspace
|
||||
</router-link>
|
||||
<button v-if="project" class="btn-share" @click="showShare = true">Share</button>
|
||||
<button v-if="project" class="btn-danger-outline" @click="showDeleteConfirm = true">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -582,6 +585,14 @@ async function confirmDelete() {
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
|
||||
<ShareDialog
|
||||
v-if="showShare && project"
|
||||
resource-type="project"
|
||||
:resource-id="project.id"
|
||||
:resource-title="project.title"
|
||||
@close="showShare = false"
|
||||
/>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -634,6 +645,19 @@ async function confirmDelete() {
|
||||
}
|
||||
.btn-workspace:hover { box-shadow: 0 4px 14px rgba(99, 102, 241, 0.42); opacity: 0.95; color: #fff; }
|
||||
|
||||
.btn-share {
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: none;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-share:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
|
||||
.btn-danger-outline {
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: none;
|
||||
|
||||
Reference in New Issue
Block a user