feat: share a single note
Signed-off-by: Luka Trovic <luka@nextcloud.com>
This commit is contained in:
parent
e851a336ed
commit
a7a15f6fc7
|
@ -9,6 +9,10 @@ use OCP\AppFramework\Bootstrap\IBootContext;
|
|||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
|
||||
use OCP\EventDispatcher\GenericEvent;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Share\Events\BeforeShareCreatedEvent;
|
||||
use OCP\Share\IShare;
|
||||
|
||||
class Application extends App implements IBootstrap {
|
||||
public const APP_ID = 'notes';
|
||||
|
@ -26,6 +30,20 @@ class Application extends App implements IBootstrap {
|
|||
BeforeTemplateRenderedEvent::class,
|
||||
BeforeTemplateRenderedListener::class
|
||||
);
|
||||
if (\OCA\OpenProject\Service\class_exists(BeforeShareCreatedEvent::class)) {
|
||||
$context->registerEventListener(
|
||||
BeforeShareCreatedEvent::class,
|
||||
BeforeShareCreatedListener::class
|
||||
);
|
||||
}
|
||||
// FIXME: Remove once Nextcloud 28 is the minimum supported version
|
||||
\OCP\Server::get(IEventDispatcher::class)->addListener('OCP\Share::preShare', function (GenericEvent $event) {
|
||||
/** @var IShare $share */
|
||||
$share = $event->getSubject();
|
||||
|
||||
$modernListener = \OCP\Server::get(BeforeShareCreatedListener::class);
|
||||
$modernListener->overwriteShareTarget($share);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Notes\AppInfo;
|
||||
|
||||
use OCA\Notes\Service\NoteUtil;
|
||||
use OCA\Notes\Service\SettingsService;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\File;
|
||||
use OCP\Share\Events\BeforeShareCreatedEvent;
|
||||
use OCP\Share\IShare;
|
||||
|
||||
class BeforeShareCreatedListener implements IEventListener {
|
||||
private SettingsService $settings;
|
||||
private NoteUtil $noteUtil;
|
||||
|
||||
public function __construct(SettingsService $settings, NoteUtil $noteUtil) {
|
||||
$this->settings = $settings;
|
||||
$this->noteUtil = $noteUtil;
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof BeforeShareCreatedEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->overwriteShareTarget($event->getShare());
|
||||
}
|
||||
|
||||
public function overwriteShareTarget(IShare $share): void {
|
||||
$itemType = $share->getNode() instanceof File ? 'file' : 'folder';
|
||||
$fileSourcePath = $share->getNode()->getPath();
|
||||
$itemTarget = $share->getTarget();
|
||||
$uidOwner = $share->getSharedBy();
|
||||
$ownerPath = $this->noteUtil->getRoot()->getUserFolder($uidOwner)->getPath();
|
||||
$ownerNotesPath = $ownerPath . '/' . $this->settings->get($uidOwner, 'notesPath');
|
||||
|
||||
$receiver = $share->getSharedWith();
|
||||
$receiverPath = $this->noteUtil->getRoot()->getUserFolder($receiver)->getPath();
|
||||
$receiverNotesInternalPath = $this->settings->get($receiver, 'notesPath');
|
||||
$receiverNotesPath = $receiverPath . '/' . $receiverNotesInternalPath;
|
||||
$this->noteUtil->getOrCreateFolder($receiverNotesPath);
|
||||
|
||||
if ($itemType !== 'file' || strpos($fileSourcePath, $ownerNotesPath) !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$share->setTarget('/' . $receiverNotesInternalPath . $itemTarget);
|
||||
}
|
||||
}
|
|
@ -4,11 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Notes\Controller;
|
||||
|
||||
use OCA\Files\Event\LoadSidebar;
|
||||
use OCA\Notes\AppInfo\Application;
|
||||
use OCA\Notes\Service\NotesService;
|
||||
|
||||
use OCA\Notes\Service\SettingsService;
|
||||
use OCA\Text\Event\LoadEditor;
|
||||
use OCA\Viewer\Event\LoadViewer;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
|
@ -66,6 +68,14 @@ class PageController extends Controller {
|
|||
$this->eventDispatcher->dispatchTyped(new LoadEditor());
|
||||
}
|
||||
|
||||
if (class_exists(LoadSidebar::class)) {
|
||||
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
|
||||
}
|
||||
|
||||
if (\OCP\Server::get(IAppManager::class)->isEnabledForUser('viewer') && class_exists(LoadViewer::class)) {
|
||||
$this->eventDispatcher->dispatchTyped(new LoadViewer());
|
||||
}
|
||||
|
||||
$this->initialState->provideInitialState(
|
||||
'config',
|
||||
(array)\OCP\Server::get(SettingsService::class)->getPublic($this->userSession->getUser()->getUID())
|
||||
|
|
|
@ -105,6 +105,9 @@ class Note {
|
|||
if (!in_array('readonly', $exclude)) {
|
||||
$data['readonly'] = $this->getReadOnly();
|
||||
}
|
||||
$data['internalPath'] = $this->noteUtil->getPathForUser($this->file);
|
||||
$data['shareTypes'] = $this->noteUtil->getShareTypes($this->file);
|
||||
$data['isShared'] = (bool) count($data['shareTypes']);
|
||||
$data['error'] = false;
|
||||
$data['errorType'] = '';
|
||||
if (!in_array('content', $exclude)) {
|
||||
|
|
|
@ -4,10 +4,14 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Notes\Service;
|
||||
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Node;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Share\IManager;
|
||||
use OCP\Share\IShare;
|
||||
|
||||
class NoteUtil {
|
||||
private const MAX_TITLE_LENGTH = 100;
|
||||
|
@ -15,23 +19,34 @@ class NoteUtil {
|
|||
private IDBConnection $db;
|
||||
private IRootFolder $root;
|
||||
private TagService $tagService;
|
||||
private IManager $shareManager;
|
||||
private IUserSession $userSession;
|
||||
|
||||
public function __construct(
|
||||
Util $util,
|
||||
IRootFolder $root,
|
||||
IDBConnection $db,
|
||||
TagService $tagService
|
||||
TagService $tagService,
|
||||
IManager $shareManager,
|
||||
IUserSession $userSession
|
||||
) {
|
||||
$this->util = $util;
|
||||
$this->root = $root;
|
||||
$this->db = $db;
|
||||
$this->tagService = $tagService;
|
||||
$this->shareManager = $shareManager;
|
||||
$this->userSession = $userSession;
|
||||
}
|
||||
|
||||
public function getRoot() : IRootFolder {
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
public function getPathForUser(File $file) {
|
||||
$userFolder = $this->root->getUserFolder($this->userSession->getUser()->getUID());
|
||||
return $userFolder->getRelativePath($file->getPath());
|
||||
}
|
||||
|
||||
public function getTagService() : TagService {
|
||||
return $this->tagService;
|
||||
}
|
||||
|
@ -203,4 +218,29 @@ class NoteUtil {
|
|||
throw new NoteNotWritableException();
|
||||
}
|
||||
}
|
||||
|
||||
public function getShareTypes(File $file): array {
|
||||
$userId = $file->getOwner()->getUID();
|
||||
$requestedShareTypes = [
|
||||
IShare::TYPE_USER,
|
||||
IShare::TYPE_GROUP,
|
||||
IShare::TYPE_LINK,
|
||||
IShare::TYPE_REMOTE,
|
||||
IShare::TYPE_EMAIL,
|
||||
IShare::TYPE_ROOM,
|
||||
IShare::TYPE_DECK,
|
||||
IShare::TYPE_SCIENCEMESH,
|
||||
];
|
||||
$shareTypes = [];
|
||||
|
||||
foreach ($requestedShareTypes as $shareType) {
|
||||
$shares = $this->shareManager->getSharesBy($userId, $shareType, $file, false, 1, 0);
|
||||
|
||||
if (count($shares)) {
|
||||
$shareTypes[] = $shareType;
|
||||
}
|
||||
}
|
||||
|
||||
return $shareTypes;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,6 +150,7 @@ export const refreshNote = (noteId, lastETag) => {
|
|||
return response.headers.etag
|
||||
}
|
||||
const currentContent = store.getters.getNote(noteId).content
|
||||
store.commit('setNoteAttribute', { noteId, attribute: 'internalPath', value: response.data.internalPath })
|
||||
// only update if local content has not changed
|
||||
if (oldContent === currentContent) {
|
||||
_updateLocalNote(response.data)
|
||||
|
@ -290,6 +291,7 @@ export const autotitleNote = noteId => {
|
|||
.put(url('/notes/' + noteId + '/autotitle'))
|
||||
.then((response) => {
|
||||
store.commit('setNoteAttribute', { noteId, attribute: 'title', value: response.data })
|
||||
refreshNote(noteId)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
|
|
|
@ -26,11 +26,21 @@
|
|||
fill-color="var(--color-text-lighter)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="isShared" #indicator>
|
||||
<ShareVariantIcon :size="16" fill-color="#0082c9" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<NcActionButton :icon="actionFavoriteIcon" @click="onToggleFavorite">
|
||||
{{ actionFavoriteText }}
|
||||
</NcActionButton>
|
||||
|
||||
<NcActionButton @click="onToggleSharing">
|
||||
<template #icon>
|
||||
<ShareVariantIcon :size="20" />
|
||||
</template>
|
||||
{{ t('notes', 'Share') }}
|
||||
</NcActionButton>
|
||||
|
||||
<NcActionButton v-if="!showCategorySelect" @click="showCategorySelect = true">
|
||||
<template #icon>
|
||||
<FolderIcon :size="20" />
|
||||
|
@ -90,6 +100,8 @@ import StarIcon from 'vue-material-design-icons/Star.vue'
|
|||
import { categoryLabel, routeIsNewNote } from '../Util.js'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { setFavorite, setTitle, fetchNote, deleteNote, setCategory } from '../NotesService.js'
|
||||
import ShareVariantIcon from 'vue-material-design-icons/ShareVariant.vue'
|
||||
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
|
||||
export default {
|
||||
name: 'NoteItem',
|
||||
|
@ -104,6 +116,7 @@ export default {
|
|||
NcActionSeparator,
|
||||
NcActionInput,
|
||||
PencilIcon,
|
||||
ShareVariantIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -122,6 +135,7 @@ export default {
|
|||
newTitle: '',
|
||||
renaming: false,
|
||||
showCategorySelect: false,
|
||||
isShareCreated: false,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -129,6 +143,9 @@ export default {
|
|||
isSelected() {
|
||||
return this.$store.getters.getSelectedNote() === this.note.id
|
||||
},
|
||||
isShared() {
|
||||
return this.note.isShared || this.isShareCreated
|
||||
},
|
||||
|
||||
title() {
|
||||
return this.note.title + (this.note.unsaved ? ' *' : '')
|
||||
|
@ -167,6 +184,15 @@ export default {
|
|||
]
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
subscribe('files_sharing:share:created', this.onShareCreated)
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
unsubscribe('files_sharing:share:created', this.onShareCreated)
|
||||
},
|
||||
|
||||
methods: {
|
||||
onMenuChange(state) {
|
||||
this.actionsOpen = state
|
||||
|
@ -251,6 +277,23 @@ export default {
|
|||
this.actionsOpen = false
|
||||
}
|
||||
},
|
||||
onToggleSharing() {
|
||||
if (window?.OCA?.Files?.Sidebar?.setActiveTab) {
|
||||
emit('toggle-navigation', { open: false })
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
}, 200)
|
||||
window.OCA.Files.Sidebar.setActiveTab('sharing')
|
||||
window.OCA.Files.Sidebar.open(this.note.internalPath)
|
||||
}
|
||||
},
|
||||
async onShareCreated(event) {
|
||||
const { share } = event
|
||||
|
||||
if (share.fileSource === this.note.id) {
|
||||
this.isShareCreated = true
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -96,7 +96,7 @@ import {
|
|||
isMobile,
|
||||
} from '@nextcloud/vue'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
|
||||
import EditIcon from 'vue-material-design-icons/LeadPencil.vue'
|
||||
import EyeIcon from 'vue-material-design-icons/Eye.vue'
|
||||
|
@ -193,6 +193,8 @@ export default {
|
|||
document.addEventListener('fullscreenchange', this.onDetectFullscreen)
|
||||
document.addEventListener('keydown', this.onKeyPress)
|
||||
document.addEventListener('visibilitychange', this.onVisibilityChange)
|
||||
subscribe('files_versions:restore:requested', this.onFileRestoreRequested)
|
||||
subscribe('files_versions:restore:restored', this.onFileRestored)
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
|
@ -203,6 +205,8 @@ export default {
|
|||
document.removeEventListener('keydown', this.onKeyPress)
|
||||
document.removeEventListener('visibilitychange', this.onVisibilityChange)
|
||||
this.onUpdateTitle(null)
|
||||
unsubscribe('files_versions:restore:requested', this.onFileRestoreRequested)
|
||||
unsubscribe('files_versions:restore:restored', this.onFileRestored)
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -392,6 +396,25 @@ export default {
|
|||
conflictSolutionRemote(this.note)
|
||||
this.showConflict = false
|
||||
},
|
||||
|
||||
async onFileRestoreRequested(event) {
|
||||
const { fileInfo } = event
|
||||
|
||||
if (fileInfo.id !== this.note.id) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
},
|
||||
|
||||
async onFileRestored(version) {
|
||||
if (version.fileId !== this.note.id) {
|
||||
return
|
||||
}
|
||||
|
||||
this.refreshNote()
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '@nextcloud/vue'
|
||||
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
|
||||
import { queueCommand } from '../NotesService.js'
|
||||
import { queueCommand, refreshNote } from '../NotesService.js'
|
||||
import { routeIsNewNote } from '../Util.js'
|
||||
import store from '../store.js'
|
||||
|
||||
|
@ -55,12 +55,15 @@ export default {
|
|||
created() {
|
||||
this.fetchData()
|
||||
subscribe('files:file:updated', this.fileUpdated)
|
||||
|
||||
subscribe('files_versions:restore:requested', this.onFileRestoreRequested)
|
||||
subscribe('files_versions:restore:restored', this.onFileRestored)
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this?.editor?.destroy()
|
||||
unsubscribe('files:file:updated', this.fileUpdated)
|
||||
unsubscribe('files_versions:restore:requested', this.onFileRestoreRequested)
|
||||
unsubscribe('files_versions:restore:restored', this.onFileRestored)
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -141,6 +144,38 @@ export default {
|
|||
.replaceAll(/\s/gmu, ' ')
|
||||
return title.length > 0 ? title : t('notes', 'New note')
|
||||
},
|
||||
|
||||
async onFileRestoreRequested(event) {
|
||||
const { fileInfo } = event
|
||||
|
||||
if (fileInfo.id !== this.note.id) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
},
|
||||
|
||||
async onFileRestored(version) {
|
||||
if (version.fileId !== this.note.id) {
|
||||
return
|
||||
}
|
||||
|
||||
const etag = await refreshNote(parseInt(this.noteId), this.etag)
|
||||
|
||||
if (etag) {
|
||||
this.etag = etag
|
||||
}
|
||||
|
||||
const autoResolve = setInterval(() => {
|
||||
const el = document.querySelector('[data-cy="resolveServerVersion"]')
|
||||
|
||||
if (el) {
|
||||
el.click()
|
||||
clearInterval(autoResolve)
|
||||
}
|
||||
}, 200)
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue