feat: Vor-/Nachname, geteilte Listen zeigen Eigentuemer
Backend: - User.first_name / User.last_name (nullable, Auto-Migrate fuegt sie an) full_name/display_name als Properties + in to_dict - TaskList.owner-Relationship ergaenzt (fehlte, daher wurden geteilte Listen beim Empfaenger nicht korrekt aufgeloest) - /auth/me GET + PUT (Profil bearbeiten: Vorname, Nachname, E-Mail) - /users/search findet jetzt auch nach Vor-/Nachname und liefert full_name/display_name mit - list_tasklists/list_calendars/list_addressbooks liefern owner_full_name und owner_display_name Frontend: - Sidebars bei Kontakten/Kalender/Aufgaben: "(geteilt von <Voller Name>)" mit Fallback auf Username - User-Search-Popup zeigt vollen Namen neben Username - SettingsView: Vorname/Nachname/E-Mail bearbeiten Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e4dd555bd1
commit
4d67819cac
|
|
@ -109,6 +109,8 @@ def list_calendars():
|
||||||
if share and share.color:
|
if share and share.color:
|
||||||
d['color'] = share.color
|
d['color'] = share.color
|
||||||
d['owner_name'] = c.owner.username
|
d['owner_name'] = c.owner.username
|
||||||
|
d['owner_full_name'] = c.owner.full_name
|
||||||
|
d['owner_display_name'] = c.owner.display_name
|
||||||
result.append(d)
|
result.append(d)
|
||||||
|
|
||||||
return jsonify(result), 200
|
return jsonify(result), 200
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,8 @@ def list_addressbooks():
|
||||||
if share and share.color:
|
if share and share.color:
|
||||||
d['color'] = share.color
|
d['color'] = share.color
|
||||||
d['owner_name'] = b.owner.username
|
d['owner_name'] = b.owner.username
|
||||||
|
d['owner_full_name'] = b.owner.full_name
|
||||||
|
d['owner_display_name'] = b.owner.display_name
|
||||||
d['contact_count'] = b.contacts.count()
|
d['contact_count'] = b.contacts.count()
|
||||||
result.append(d)
|
result.append(d)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,10 @@ def list_tasklists():
|
||||||
continue
|
continue
|
||||||
d = tl.to_dict()
|
d = tl.to_dict()
|
||||||
d['permission'] = s.permission
|
d['permission'] = s.permission
|
||||||
d['owner_name'] = tl.owner.username if tl.owner else ''
|
owner = tl.owner
|
||||||
|
d['owner_name'] = owner.username if owner else ''
|
||||||
|
d['owner_full_name'] = owner.full_name if owner else ''
|
||||||
|
d['owner_display_name'] = owner.display_name if owner else ''
|
||||||
d['task_count'] = tl.tasks.count()
|
d['task_count'] = tl.tasks.count()
|
||||||
if s.color:
|
if s.color:
|
||||||
d['color'] = s.color
|
d['color'] = s.color
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,31 @@ def create_invite_link():
|
||||||
|
|
||||||
# --- User search (for sharing dialogs) ---
|
# --- User search (for sharing dialogs) ---
|
||||||
|
|
||||||
|
@api_bp.route('/auth/me', methods=['GET'])
|
||||||
|
@token_required
|
||||||
|
def get_me():
|
||||||
|
return jsonify(request.current_user.to_dict(include_email=True)), 200
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route('/auth/me', methods=['PUT'])
|
||||||
|
@token_required
|
||||||
|
def update_me():
|
||||||
|
user = request.current_user
|
||||||
|
data = request.get_json() or {}
|
||||||
|
if 'first_name' in data:
|
||||||
|
user.first_name = (data.get('first_name') or '').strip() or None
|
||||||
|
if 'last_name' in data:
|
||||||
|
user.last_name = (data.get('last_name') or '').strip() or None
|
||||||
|
if 'email' in data:
|
||||||
|
email = (data.get('email') or '').strip() or None
|
||||||
|
if email and email != user.email:
|
||||||
|
if User.query.filter(User.email == email, User.id != user.id).first():
|
||||||
|
return jsonify({'error': 'E-Mail ist bereits vergeben'}), 409
|
||||||
|
user.email = email
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(user.to_dict(include_email=True)), 200
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route('/users/search', methods=['GET'])
|
@api_bp.route('/users/search', methods=['GET'])
|
||||||
@token_required
|
@token_required
|
||||||
def search_users():
|
def search_users():
|
||||||
|
|
@ -289,13 +314,19 @@ def search_users():
|
||||||
if len(query) < 2:
|
if len(query) < 2:
|
||||||
return jsonify([]), 200
|
return jsonify([]), 200
|
||||||
|
|
||||||
|
like = f'%{query}%'
|
||||||
users = User.query.filter(
|
users = User.query.filter(
|
||||||
User.username.ilike(f'%{query}%'),
|
(User.username.ilike(like)) | (User.first_name.ilike(like)) | (User.last_name.ilike(like)),
|
||||||
User.id != request.current_user.id,
|
User.id != request.current_user.id,
|
||||||
User.is_active == True,
|
User.is_active == True,
|
||||||
).limit(10).all()
|
).limit(10).all()
|
||||||
|
|
||||||
return jsonify([{'id': u.id, 'username': u.username} for u in users]), 200
|
return jsonify([{
|
||||||
|
'id': u.id,
|
||||||
|
'username': u.username,
|
||||||
|
'full_name': u.full_name,
|
||||||
|
'display_name': u.display_name,
|
||||||
|
} for u in users]), 200
|
||||||
|
|
||||||
|
|
||||||
# --- Change password (non-admin, own account) ---
|
# --- Change password (non-admin, own account) ---
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ class User(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
|
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
|
||||||
email = db.Column(db.String(255), unique=True, nullable=True)
|
email = db.Column(db.String(255), unique=True, nullable=True)
|
||||||
|
first_name = db.Column(db.String(100), nullable=True)
|
||||||
|
last_name = db.Column(db.String(100), nullable=True)
|
||||||
password_hash = db.Column(db.String(255), nullable=False)
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
role = db.Column(db.String(20), default='user', nullable=False) # 'admin' or 'user'
|
role = db.Column(db.String(20), default='user', nullable=False) # 'admin' or 'user'
|
||||||
master_key_salt = db.Column(db.LargeBinary, nullable=True) # For password manager
|
master_key_salt = db.Column(db.LargeBinary, nullable=True) # For password manager
|
||||||
|
|
@ -23,6 +25,7 @@ class User(db.Model):
|
||||||
foreign_keys='File.owner_id')
|
foreign_keys='File.owner_id')
|
||||||
calendars = db.relationship('Calendar', backref='owner', lazy='dynamic')
|
calendars = db.relationship('Calendar', backref='owner', lazy='dynamic')
|
||||||
address_books = db.relationship('AddressBook', backref='owner', lazy='dynamic')
|
address_books = db.relationship('AddressBook', backref='owner', lazy='dynamic')
|
||||||
|
task_lists = db.relationship('TaskList', backref='owner', lazy='dynamic')
|
||||||
email_accounts = db.relationship('EmailAccount', backref='user', lazy='dynamic',
|
email_accounts = db.relationship('EmailAccount', backref='user', lazy='dynamic',
|
||||||
order_by='EmailAccount.sort_order')
|
order_by='EmailAccount.sort_order')
|
||||||
password_folders = db.relationship('PasswordFolder', backref='owner', lazy='dynamic')
|
password_folders = db.relationship('PasswordFolder', backref='owner', lazy='dynamic')
|
||||||
|
|
@ -33,10 +36,25 @@ class User(db.Model):
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return bcrypt.check_password_hash(self.password_hash, password)
|
return bcrypt.check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self) -> str:
|
||||||
|
"""Vor- + Nachname zusammengesetzt, sonst Leerstring."""
|
||||||
|
parts = [self.first_name or '', self.last_name or '']
|
||||||
|
return ' '.join(p.strip() for p in parts if p and p.strip())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self) -> str:
|
||||||
|
"""Voller Name falls vorhanden, sonst Username."""
|
||||||
|
return self.full_name or self.username
|
||||||
|
|
||||||
def to_dict(self, include_email=False):
|
def to_dict(self, include_email=False):
|
||||||
data = {
|
data = {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'username': self.username,
|
'username': self.username,
|
||||||
|
'first_name': self.first_name or '',
|
||||||
|
'last_name': self.last_name or '',
|
||||||
|
'full_name': self.full_name,
|
||||||
|
'display_name': self.display_name,
|
||||||
'role': self.role,
|
'role': self.role,
|
||||||
'is_active': self.is_active,
|
'is_active': self.is_active,
|
||||||
'storage_quota_mb': self.storage_quota_mb,
|
'storage_quota_mb': self.storage_quota_mb,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@
|
||||||
<input type="checkbox" v-model="visibleCalendars[cal.id]" @change="refreshEvents" />
|
<input type="checkbox" v-model="visibleCalendars[cal.id]" @change="refreshEvents" />
|
||||||
<div class="calendar-color" :style="{ background: cal.color }"></div>
|
<div class="calendar-color" :style="{ background: cal.color }"></div>
|
||||||
<span class="cal-name">{{ cal.name }}</span>
|
<span class="cal-name">{{ cal.name }}</span>
|
||||||
<span v-if="cal.permission !== 'owner'" class="shared-label">(geteilt)</span>
|
<span v-if="cal.permission !== 'owner'" class="shared-label"
|
||||||
|
:title="`Geteilt von ${cal.owner_display_name || cal.owner_name}`">
|
||||||
|
(geteilt von {{ cal.owner_display_name || cal.owner_name }})
|
||||||
|
</span>
|
||||||
<Button icon="pi pi-ellipsis-v" text size="small" @click.stop="openCalendarMenu(cal)" />
|
<Button icon="pi pi-ellipsis-v" text size="small" @click.stop="openCalendarMenu(cal)" />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
@ -276,7 +279,9 @@
|
||||||
<div v-if="shareSearchResults.length" class="user-search-popup">
|
<div v-if="shareSearchResults.length" class="user-search-popup">
|
||||||
<div v-for="u in shareSearchResults" :key="u.id" class="user-result"
|
<div v-for="u in shareSearchResults" :key="u.id" class="user-result"
|
||||||
@click="shareUsername = u.username; shareSearchResults = []">
|
@click="shareUsername = u.username; shareSearchResults = []">
|
||||||
<i class="pi pi-user"></i> {{ u.username }}
|
<i class="pi pi-user"></i>
|
||||||
|
<span>{{ u.username }}</span>
|
||||||
|
<small v-if="u.full_name" class="user-fullname">{{ u.full_name }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1292,6 +1297,7 @@ onUnmounted(() => {
|
||||||
max-height: 160px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
max-height: 160px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
||||||
.user-result { padding: 0.5rem 0.75rem; cursor: pointer; font-size: 0.875rem; display: flex; gap: 0.5rem; align-items: center; }
|
.user-result { padding: 0.5rem 0.75rem; cursor: pointer; font-size: 0.875rem; display: flex; gap: 0.5rem; align-items: center; }
|
||||||
.user-result:hover { background: var(--p-primary-50); }
|
.user-result:hover { background: var(--p-primary-50); }
|
||||||
|
.user-fullname { color: var(--p-text-muted-color); font-size: 0.75rem; margin-left: auto; }
|
||||||
.existing-shares { margin-top: 0.5rem; }
|
.existing-shares { margin-top: 0.5rem; }
|
||||||
.share-perm-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0; font-size: 0.875rem; flex-wrap: wrap; }
|
.share-perm-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0; font-size: 0.875rem; flex-wrap: wrap; }
|
||||||
.share-perm-item.editing { background: var(--p-surface-50); padding: 0.5rem; border-radius: 4px; }
|
.share-perm-item.editing { background: var(--p-surface-50); padding: 0.5rem; border-radius: 4px; }
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@
|
||||||
@click="selectBook(book.id)">
|
@click="selectBook(book.id)">
|
||||||
<span class="book-color" :style="{ background: book.color }"></span>
|
<span class="book-color" :style="{ background: book.color }"></span>
|
||||||
<span class="book-name">{{ book.name }}</span>
|
<span class="book-name">{{ book.name }}</span>
|
||||||
<span v-if="book.permission !== 'owner'" class="shared-label">(geteilt)</span>
|
<span v-if="book.permission !== 'owner'" class="shared-label"
|
||||||
|
:title="`Geteilt von ${book.owner_display_name || book.owner_name}`">
|
||||||
|
(geteilt von {{ book.owner_display_name || book.owner_name }})
|
||||||
|
</span>
|
||||||
<span class="count">{{ book.contact_count }}</span>
|
<span class="count">{{ book.contact_count }}</span>
|
||||||
<Button icon="pi pi-ellipsis-v" text size="small" class="book-menu"
|
<Button icon="pi pi-ellipsis-v" text size="small" class="book-menu"
|
||||||
@click.stop="openBookMenu(book)" />
|
@click.stop="openBookMenu(book)" />
|
||||||
|
|
@ -112,7 +115,9 @@
|
||||||
<div v-if="shareSearchResults.length" class="user-search-popup">
|
<div v-if="shareSearchResults.length" class="user-search-popup">
|
||||||
<div v-for="u in shareSearchResults" :key="u.id" class="user-result"
|
<div v-for="u in shareSearchResults" :key="u.id" class="user-result"
|
||||||
@click="shareUsername = u.username; shareSearchResults = []">
|
@click="shareUsername = u.username; shareSearchResults = []">
|
||||||
<i class="pi pi-user"></i> {{ u.username }}
|
<i class="pi pi-user"></i>
|
||||||
|
<span>{{ u.username }}</span>
|
||||||
|
<small v-if="u.full_name" class="user-fullname">{{ u.full_name }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -875,6 +880,7 @@ watch(selectedBookId, loadContacts)
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
||||||
.user-result { padding: 0.5rem 0.75rem; cursor: pointer; font-size: 0.875rem; }
|
.user-result { padding: 0.5rem 0.75rem; cursor: pointer; font-size: 0.875rem; }
|
||||||
.user-result:hover { background: var(--p-primary-50); }
|
.user-result:hover { background: var(--p-primary-50); }
|
||||||
|
.user-fullname { color: var(--p-text-muted-color); font-size: 0.75rem; margin-left: auto; }
|
||||||
.existing-shares { margin-top: 0.5rem; }
|
.existing-shares { margin-top: 0.5rem; }
|
||||||
.share-perm-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0; font-size: 0.875rem; flex-wrap: wrap; }
|
.share-perm-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0; font-size: 0.875rem; flex-wrap: wrap; }
|
||||||
.share-perm-item.editing { background: var(--p-surface-50); padding: 0.5rem; border-radius: 4px; }
|
.share-perm-item.editing { background: var(--p-surface-50); padding: 0.5rem; border-radius: 4px; }
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,31 @@
|
||||||
<span class="label">Benutzername:</span>
|
<span class="label">Benutzername:</span>
|
||||||
<span>{{ auth.user?.username }}</span>
|
<span>{{ auth.user?.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
|
||||||
<span class="label">E-Mail:</span>
|
|
||||||
<span>{{ auth.user?.email || 'Nicht angegeben' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="label">Rolle:</span>
|
<span class="label">Rolle:</span>
|
||||||
<Tag :value="auth.user?.role" :severity="auth.user?.role === 'admin' ? 'danger' : 'info'" />
|
<Tag :value="auth.user?.role" :severity="auth.user?.role === 'admin' ? 'danger' : 'info'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="hint" style="margin:0.75rem 0 0.5rem;font-size:0.8rem;color:var(--p-text-muted-color)">
|
||||||
|
Vor- und Nachname werden anderen Benutzern angezeigt, wenn du etwas mit ihnen teilst.
|
||||||
|
</p>
|
||||||
|
<form @submit.prevent="saveProfile" class="profile-form">
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Vorname</label>
|
||||||
|
<InputText v-model="profile.first_name" fluid />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Nachname</label>
|
||||||
|
<InputText v-model="profile.last_name" fluid />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>E-Mail</label>
|
||||||
|
<InputText v-model="profile.email" type="email" fluid />
|
||||||
|
</div>
|
||||||
|
<Button type="submit" label="Profil speichern" :loading="profileLoading" size="small" />
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Change Password -->
|
<!-- Change Password -->
|
||||||
|
|
@ -192,6 +208,36 @@ function downloadClient(client) {
|
||||||
window.location.href = `/api/clients/${client.platform}/download`
|
window.location.href = `/api/clients/${client.platform}/download`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Profile (Vorname/Nachname/E-Mail) ---
|
||||||
|
const profile = ref({ first_name: '', last_name: '', email: '' })
|
||||||
|
const profileLoading = ref(false)
|
||||||
|
|
||||||
|
async function loadProfile() {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get('/auth/me')
|
||||||
|
profile.value = {
|
||||||
|
first_name: res.data.first_name || '',
|
||||||
|
last_name: res.data.last_name || '',
|
||||||
|
email: res.data.email || '',
|
||||||
|
}
|
||||||
|
auth.user = { ...auth.user, ...res.data }
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProfile() {
|
||||||
|
profileLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await apiClient.put('/auth/me', profile.value)
|
||||||
|
auth.user = { ...auth.user, ...res.data }
|
||||||
|
toast.add({ severity: 'success', summary: 'Profil gespeichert', life: 2500 })
|
||||||
|
} catch (err) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Fehler',
|
||||||
|
detail: err.response?.data?.error || err.message, life: 4000 })
|
||||||
|
} finally {
|
||||||
|
profileLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Password change ---
|
// --- Password change ---
|
||||||
const currentPassword = ref('')
|
const currentPassword = ref('')
|
||||||
const newPassword = ref('')
|
const newPassword = ref('')
|
||||||
|
|
@ -334,6 +380,7 @@ async function doDeleteAccount() {
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loadAccounts()
|
loadAccounts()
|
||||||
|
loadProfile()
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get('/clients')
|
const res = await apiClient.get('/clients')
|
||||||
availableClients.value = res.data.clients
|
availableClients.value = res.data.clients
|
||||||
|
|
@ -352,6 +399,10 @@ onMounted(async () => {
|
||||||
.section-header h3 { margin: 0; }
|
.section-header h3 { margin: 0; }
|
||||||
.settings-info { display: flex; flex-direction: column; gap: 0.5rem; }
|
.settings-info { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||||
.info-row { display: flex; align-items: center; gap: 0.5rem; }
|
.info-row { display: flex; align-items: center; gap: 0.5rem; }
|
||||||
|
.profile-form { display: flex; flex-direction: column; gap: 0.5rem; max-width: 540px; }
|
||||||
|
.profile-form .field-row { display: flex; gap: 0.75rem; }
|
||||||
|
.profile-form .field-row .field { flex: 1; }
|
||||||
|
.profile-form .field label { display: block; font-size: 0.8rem; margin-bottom: 0.25rem; }
|
||||||
.info-row .label { font-weight: 500; min-width: 120px; }
|
.info-row .label { font-weight: 500; min-width: 120px; }
|
||||||
.password-form { max-width: 400px; }
|
.password-form { max-width: 400px; }
|
||||||
.password-form .field { margin-bottom: 1rem; }
|
.password-form .field { margin-bottom: 1rem; }
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@
|
||||||
@click="selectedListId = tl.id">
|
@click="selectedListId = tl.id">
|
||||||
<span class="list-color" :style="{ background: tl.color }"></span>
|
<span class="list-color" :style="{ background: tl.color }"></span>
|
||||||
<span class="list-name">{{ tl.name }}</span>
|
<span class="list-name">{{ tl.name }}</span>
|
||||||
<span v-if="tl.permission !== 'owner'" class="shared-label">(geteilt)</span>
|
<span v-if="tl.permission !== 'owner'" class="shared-label"
|
||||||
|
:title="`Geteilt von ${tl.owner_display_name || tl.owner_name}`">
|
||||||
|
(geteilt von {{ tl.owner_display_name || tl.owner_name }})
|
||||||
|
</span>
|
||||||
<span class="count">{{ tl.task_count }}</span>
|
<span class="count">{{ tl.task_count }}</span>
|
||||||
<Button icon="pi pi-ellipsis-v" text size="small" class="list-menu"
|
<Button icon="pi pi-ellipsis-v" text size="small" class="list-menu"
|
||||||
@click.stop="openListMenu(tl)" />
|
@click.stop="openListMenu(tl)" />
|
||||||
|
|
@ -119,7 +122,9 @@
|
||||||
<div v-if="shareSearchResults.length" class="user-search-popup">
|
<div v-if="shareSearchResults.length" class="user-search-popup">
|
||||||
<div v-for="u in shareSearchResults" :key="u.id" class="user-result"
|
<div v-for="u in shareSearchResults" :key="u.id" class="user-result"
|
||||||
@click="shareUsername = u.username; shareSearchResults = []">
|
@click="shareUsername = u.username; shareSearchResults = []">
|
||||||
<i class="pi pi-user"></i> {{ u.username }}
|
<i class="pi pi-user"></i>
|
||||||
|
<span>{{ u.username }}</span>
|
||||||
|
<small v-if="u.full_name" class="user-fullname">{{ u.full_name }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -686,6 +691,7 @@ watch(selectedListId, loadTasks)
|
||||||
.user-result { padding: 0.5rem 0.75rem; cursor: pointer; font-size: 0.875rem;
|
.user-result { padding: 0.5rem 0.75rem; cursor: pointer; font-size: 0.875rem;
|
||||||
display: flex; gap: 0.5rem; align-items: center; }
|
display: flex; gap: 0.5rem; align-items: center; }
|
||||||
.user-result:hover { background: var(--p-primary-50); }
|
.user-result:hover { background: var(--p-primary-50); }
|
||||||
|
.user-fullname { color: var(--p-text-muted-color); font-size: 0.75rem; margin-left: auto; }
|
||||||
.existing-shares { margin-top: 0.5rem; }
|
.existing-shares { margin-top: 0.5rem; }
|
||||||
.share-perm-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0; font-size: 0.875rem; flex-wrap: wrap; }
|
.share-perm-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0; font-size: 0.875rem; flex-wrap: wrap; }
|
||||||
.share-perm-item.editing { background: var(--p-surface-50); padding: 0.5rem; border-radius: 4px; }
|
.share-perm-item.editing { background: var(--p-surface-50); padding: 0.5rem; border-radius: 4px; }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue