Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions client/src/components/EditBookmark/EditBookmark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface EditBookmarkProps {
export const EditBookmark = ({ bookmark, onSave, onClose }: EditBookmarkProps) => {
const modalRef = useRef<HTMLDivElement>(null);
const [title, setTitle] = useState('');
const [isPublic, setIsPublic] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [saveErrorStatus, setSaveErrorStatus] = useState<number | null>(null);
Expand All @@ -37,6 +38,7 @@ export const EditBookmark = ({ bookmark, onSave, onClose }: EditBookmarkProps) =
setSaveErrorStatus(null);
if (bookmark) {
setTitle(bookmark.title);
setIsPublic(bookmark.isPublic);
}
onClose();
}, [bookmark, onClose]);
Expand All @@ -45,8 +47,9 @@ export const EditBookmark = ({ bookmark, onSave, onClose }: EditBookmarkProps) =
useEffect(() => {
if (bookmark) {
setTitle(bookmark.title);
setIsPublic(bookmark.isPublic);
}
}, [bookmark?.title]);
}, [bookmark?.title, bookmark?.isPublic]);

// Show modal when bookmark changes
useEffect(() => {
Expand Down Expand Up @@ -82,7 +85,7 @@ export const EditBookmark = ({ bookmark, onSave, onClose }: EditBookmarkProps) =

try {
// Update bookmark title via API
const updatedBookmark = await updateBookmark(bookmark.id, { title: title.trim() });
const updatedBookmark = await updateBookmark(bookmark.id, { title: title.trim(), isPublic });

// Dispatch custom events to notify other components
// Pass the updated bookmark in the event detail to avoid full page reload
Expand Down Expand Up @@ -139,6 +142,19 @@ export const EditBookmark = ({ bookmark, onSave, onClose }: EditBookmarkProps) =
autoFocus
/>
</div>
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
id="bookmark-is-public-input"
checked={isPublic}
onChange={(e) => setIsPublic(e.target.checked)}
disabled={isSaving}
/>
<label className="form-check-label" htmlFor="bookmark-is-public-input">
Public
</label>
</div>
</div>
<div className="modal-footer">
<button
Expand All @@ -152,7 +168,7 @@ export const EditBookmark = ({ bookmark, onSave, onClose }: EditBookmarkProps) =
<button
type="submit"
className="btn btn-primary"
disabled={isSaving || !title.trim() || (bookmark ? title.trim() === bookmark.title : false)}
disabled={isSaving || !title.trim() || (bookmark ? (title.trim() === bookmark.title && isPublic === bookmark.isPublic) : false)}
>
{isSaving ? (
<>
Expand Down
6 changes: 6 additions & 0 deletions server/src/Api/Controller/MeBookmarkController.php
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,8 @@ public function patch(
)]
BookmarkApiDto $bookmarkPayload,
): JsonResponse {
$wasPublic = $bookmark->isPublic;

// Manual merge
if (isset($bookmarkPayload->title)) {
$bookmark->title = $bookmarkPayload->title;
Expand All @@ -461,6 +463,10 @@ public function patch(
$this->entityManager->persist($indexAction);

$this->entityManager->flush();

if (!$wasPublic && $bookmark->isPublic) {
$this->messageBus->dispatch(new SendCreateNoteMessage($bookmark->id));
}
} catch (ORMInvalidArgumentException|ORMException $e) {
throw new UnprocessableEntityHttpException(previous: $e);
}
Expand Down
27 changes: 27 additions & 0 deletions server/tests/Api/Controller/BookmarkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,33 @@ public function testEditBookmarkTagsOnly(): void
$this->assertCount(0, $json['tags'], 'All tags should be removed');
}

public function testEditOwnBookmarkCanToggleVisibilityToPublic(): void
{
[, $token, $account] = $this->createAuthenticatedUserAccount('testuser', 'test');

$bookmark = BookmarkFactory::createOne([
'account' => $account,
'title' => 'Private Bookmark',
'url' => 'https://example.com/private',
'isPublic' => false,
]);

$this->request('PATCH', "/users/me/bookmarks/{$bookmark->id}", [
'headers' => ['Content-Type' => 'application/json'],
'auth_bearer' => $token,
'json' => [
'isPublic' => true,
],
]);
$this->assertResponseIsSuccessful();

$json = $this->dump($this->getResponseArray());
$this->assertTrue($json['isPublic'], 'Bookmark should become public after PATCH update.');

$this->request('GET', "/profile/testuser/bookmarks/{$bookmark->id}");
$this->assertResponseIsSuccessful();
}

public function testDeleteOwnBookmark(): void
{
[, $token, $account] = $this->createAuthenticatedUserAccount('testuser', 'test');
Expand Down
Loading