logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: d074aefb4ffe8fc7bdb0e5f0afec46f7042a90fe
parent 38bd59ceb0182de15e2e97d750df59ad53dfa51a
Author: Henry Jameson <me@hjkos.com>
Date:   Wed, 17 Aug 2022 00:48:10 +0300

List edit UI overhaul

Diffstat:

Msrc/App.js4++++
Msrc/App.vue2+-
Msrc/components/lists_edit/lists_edit.js80++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/components/lists_edit/lists_edit.vue210++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/components/lists_user_search/lists_user_search.js7++++++-
Msrc/components/lists_user_search/lists_user_search.vue20+++++++++++---------
Msrc/components/mobile_post_status_button/mobile_post_status_button.js3++-
Msrc/components/tab_switcher/tab_switcher.scss1+
Msrc/i18n/en.json13++++++++++++-
9 files changed, 248 insertions(+), 92 deletions(-)

diff --git a/src/App.js b/src/App.js @@ -85,8 +85,12 @@ export default { isChats () { return this.$route.name === 'chat' || this.$route.name === 'chats' }, + isListEdit () { + return this.$route.name === 'lists-edit' + }, newPostButtonShown () { if (this.isChats) return false + if (this.isListEdit) return false return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, diff --git a/src/App.vue b/src/App.vue @@ -33,7 +33,7 @@ <div id="main-scroller" class="column main" - :class="{ '-full-height': isChats }" + :class="{ '-full-height': isChats || isListEdit }" > <div v-if="!currentUser" diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js @@ -1,7 +1,9 @@ import { mapState, mapGetters } from 'vuex' import BasicUserCard from '../basic_user_card/basic_user_card.vue' import ListsUserSearch from '../lists_user_search/lists_user_search.vue' +import PanelLoading from 'src/components/panel_loading/panel_loading.vue' import UserAvatar from '../user_avatar/user_avatar.vue' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import { library } from '@fortawesome/fontawesome-svg-core' import { faSearch, @@ -17,22 +19,32 @@ const ListsNew = { components: { BasicUserCard, UserAvatar, - ListsUserSearch + ListsUserSearch, + TabSwitcher, + PanelLoading }, data () { return { title: '', - userIds: [], - selectedUserIds: [] + titleDraft: '', + membersUserIds: [], + removedUserIds: new Set([]), // users we added for members, to undo + searchUserIds: [], + addedUserIds: new Set([]), // users we added from search, to undo + searchLoading: false, + reallyDelete: false } }, created () { this.$store.dispatch('fetchList', { listId: this.id }) - .then(() => { this.title = this.findListTitle(this.id) }) + .then(() => { + this.title = this.findListTitle(this.id) + this.titleDraft = this.title + }) this.$store.dispatch('fetchListAccounts', { listId: this.id }) .then(() => { - this.selectedUserIds = this.findListAccounts(this.id) - this.selectedUserIds.forEach(userId => { + this.membersUserIds = this.findListAccounts(this.id) + this.membersUserIds.forEach(userId => { this.$store.dispatch('fetchUserIfMissing', userId) }) }) @@ -41,11 +53,12 @@ const ListsNew = { id () { return this.$route.params.id }, - users () { - return this.userIds.map(userId => this.findUser(userId)) + membersUsers () { + return [...this.membersUserIds, ...this.addedUserIds] + .map(userId => this.findUser(userId)).filter(user => user) }, - selectedUsers () { - return this.selectedUserIds.map(userId => this.findUser(userId)).filter(user => user) + searchUsers () { + return this.searchUserIds.map(userId => this.findUser(userId)).filter(user => user) }, ...mapState({ currentUser: state => state.users.currentUser @@ -56,30 +69,51 @@ const ListsNew = { onInput () { this.search(this.query) }, - selectUser (user) { - if (this.selectedUserIds.includes(user.id)) { + toggleRemoveMember (user) { + if (this.removedUserIds.has(user.id)) { + this.addUser(user) + this.removedUserIds.delete(user.id) + } else { + this.removeUser(user.id) + this.removedUserIds.add(user.id) + } + }, + toggleAddFromSearch (user) { + if (this.addedUserIds.has(user.id)) { this.removeUser(user.id) + this.addedUserIds.delete(user.id) } else { this.addUser(user) + this.addedUserIds.add(user.id) } }, - isSelected (user) { - return this.selectedUserIds.includes(user.id) + isRemoved (user) { + return this.removedUserIds.has(user.id) + }, + isAdded (user) { + return this.addedUserIds.has(user.id) }, addUser (user) { - this.selectedUserIds.push(user.id) + // this.selectedUserIds.push(user.id) }, removeUser (userId) { - this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId) + // this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId) }, - onResults (results) { - this.userIds = results + onSearchLoading (results) { + this.searchLoading = true }, - updateList () { - this.$store.dispatch('setList', { listId: this.id, title: this.title }) - this.$store.dispatch('setListAccounts', { listId: this.id, accountIds: this.selectedUserIds }) - - this.$router.push({ name: 'lists-timeline', params: { id: this.id } }) + onSearchLoadingDone (results) { + this.searchLoading = false + }, + onSearchResults (results) { + this.searchLoading = false + this.searchUserIds = results + }, + updateListTitle () { + this.$store.dispatch('setList', { listId: this.id, title: this.titleDraft }) + .then(() => { + this.title = this.findListTitle(this.id) + }) }, deleteList () { this.$store.dispatch('deleteList', { listId: this.id }) diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue @@ -1,8 +1,8 @@ <template> - <div class="panel-default panel list-edit"> + <div class="panel-default panel ListEdit"> <div ref="header" - class="panel-heading" + class="panel-heading list-edit-heading" > <button class="button-unstyled go-back-button" @@ -13,54 +13,129 @@ icon="chevron-left" /> </button> - </div> - <div class="input-wrap"> - <input - ref="title" - v-model="title" - :placeholder="$t('lists.title')" + <div class="title"> + <i18n-t + keypath="lists.editing_list" > + <template #listTitle>{{ title }}</template> + </i18n-t> + </div> </div> - <div class="member-list"> - <div - v-for="user in selectedUsers" - :key="user.id" - class="member" - > - <BasicUserCard - :user="user" - :class="isSelected(user) ? 'selected' : ''" - @click.capture.prevent="selectUser(user)" - /> + <div class="panel-body"> + <div class="input-wrap"> + <label for="list-edit-title">{{ $t('lists.title') }}</label> + {{ ' ' }} + <input + id="list-edit-title" + ref="title" + v-model="titleDraft" + > + <button + class="btn button-default follow-button" + @click="updateListTitle" + > + {{ $t('lists.update_title') }} + </button> </div> + <tab-switcher + class="list-member-management" + :scrollable-tabs="true" + > + <div + :label="$t('lists.manage_members')" + class="members-list" + > + <div class="users-list"> + <div + v-for="user in membersUsers" + :key="user.id" + class="member" + > + <BasicUserCard + :user="user" + > + <button + class="btn button-default follow-button" + @click="toggleRemoveMember(user)" + > + {{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }} + </button> + </BasicUserCard> + </div> + </div> + </div> + + <div + class="search-list" + :label="$t('lists.add_members')" + > + <ListsUserSearch + @results="onSearchResults" + @loading="onSearchLoading" + @loadingDone="onSearchLoadingDone" + /> + <div v-if="searchLoading" class="loading"> + <PanelLoading /> + </div> + <div v-else class="users-list"> + <div + v-for="user in searchUsers" + :key="user.id" + class="member" + > + <BasicUserCard + :user="user" + > + <span + v-if="membersUserIds.includes(user.id)" + > + {{ $t('lists.is_in_list') }} + </span> + <button + v-if="!membersUserIds.includes(user.id)" + class="btn button-default follow-button" + @click="toggleAddFromSearch(user)" + > + {{ isAdded(user) ? $t('general.undo') : $t('lists.add_to_list') }} + </button> + <button + v-else + class="btn button-default follow-button" + @click="toggleRemoveMember(user)" + > + {{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }} + </button> + </BasicUserCard> + </div> + </div> + </div> + </tab-switcher> </div> - <ListsUserSearch @results="onResults" /> - <div class="member-list"> - <div - v-for="user in users" - :key="user.id" - class="member" + <div class="panel-footer"> + <span class="spacer" /> + <button + class="btn button-default delete-button" + @click="reallyDelete = true" + v-if="!reallyDelete" > - <BasicUserCard - :user="user" - :class="isSelected(user) ? 'selected' : ''" - @click.capture.prevent="selectUser(user)" - /> - </div> + {{ $t('lists.delete') }} + </button> + <template v-else> + {{ $t('lists.really_delete') }} + <button + class="btn button-default delete-button" + @click="deleteList" + > + {{ $t('general.yes') }} + </button> + <button + class="btn button-default delete-button" + @click="reallyDelete = false" + > + {{ $t('general.no') }} + </button> + </template> </div> - <button - :disabled="title && title.length === 0" - class="btn button-default" - @click="updateList" - > - {{ $t('lists.save') }} - </button> - <button - class="btn button-default" - @click="deleteList" - > - {{ $t('lists.delete') }} - </button> </div> </template> @@ -69,28 +144,43 @@ <style lang="scss"> @import '../../_variables.scss'; -.list-edit { - .input-wrap { +.ListEdit { + --panel-body-padding: 0.5em; + + height: calc(100vh - var(--navbar-height)); + overflow: hidden; + display: flex; + flex-direction: column; + + .list-edit-heading { + grid-template-columns: auto minmax(50%, 1fr); + } + + .panel-body { display: flex; - margin: 0.7em 0.5em 0.7em 0.5em; + flex: 1; + flex-direction: column; + overflow: hidden; + } - input { - width: 100%; - } + .list-member-management { + flex: 1 0 auto; } .search-icon { margin-right: 0.3em; } - .member-list { + .users-list { padding-bottom: 0.7rem; + overflow-y: auto; } - .basic-user-card:hover, - .basic-user-card.selected { - cursor: pointer; - background-color: var(--selectedPost, $fallback--lightBg); + & .search-list, + & .members-list { + overflow: hidden; + flex-direction: column; + min-height: 0; } .go-back-button { @@ -102,7 +192,15 @@ } .btn { - margin: 0.5em; + margin: 0 0.5em; + } + + .panel-footer { + grid-template-columns: minmax(10%, 1fr); + + .delete-button { + min-width: 9em; + } } } </style> diff --git a/src/components/lists_user_search/lists_user_search.js b/src/components/lists_user_search/lists_user_search.js @@ -15,6 +15,7 @@ const ListsUserSearch = { components: { Checkbox }, + emits: ['loading', 'loadingDone', 'results'], data () { return { loading: false, @@ -33,12 +34,16 @@ const ListsUserSearch = { } this.loading = true + this.$emit('loading') this.userIds = [] this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly }) .then(data => { - this.loading = false this.$emit('results', data.accounts.map(a => a.id)) }) + .finally(() => { + this.loading = false + this.$emit('loadingDone') + }) } } } diff --git a/src/components/lists_user_search/lists_user_search.vue b/src/components/lists_user_search/lists_user_search.vue @@ -1,5 +1,5 @@ <template> - <div> + <div class="ListsUserSearch"> <div class="input-wrap"> <div class="input-search"> <FAIcon @@ -29,17 +29,19 @@ <style lang="scss"> @import '../../_variables.scss'; -.input-wrap { - display: flex; - margin: 0.7em 0.5em 0.7em 0.5em; +.ListsUserSearch { + .input-wrap { + display: flex; + margin: 0.7em 0.5em 0.7em 0.5em; - input { - width: 100%; + input { + width: 100%; + } } -} -.search-icon { - margin-right: 0.3em; + .search-icon { + margin-right: 0.3em; + } } </style> diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js @@ -10,7 +10,8 @@ library.add( const HIDDEN_FOR_PAGES = new Set([ 'chats', - 'chat' + 'chat', + 'lists-edit' ]) const MobilePostStatusButton = { diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss @@ -17,6 +17,7 @@ overflow-x: auto; padding-top: 5px; flex-direction: row; + flex: 0 0 auto; &::after, &::before { content: ''; diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -80,6 +80,9 @@ "confirm": "Confirm", "verify": "Verify", "close": "Close", + "undo": "Undo", + "yes": "Yes", + "no": "No", "peek": "Peek", "role": { "admin": "Admin", @@ -981,7 +984,15 @@ "save": "Save changes", "delete": "Delete list", "following_only": "Limit to Following", - "manage_lists": "Manage lists" + "manage_lists": "Manage lists", + "manage_members": "Manage list members", + "add_members": "Search for more users", + "remove_from_list": "Remove from list", + "add_to_list": "Add to list", + "is_in_list": "Already in list", + "editing_list": "Editing list {listTitle}", + "update_title": "Save Title", + "really_delete": "Really delete list?" }, "file_type": { "audio": "Audio",