Skip to content

Commit

Permalink
Add feature to edit notes. (#61)
Browse files Browse the repository at this point in the history
* Add edit note feature, commit 1.

* Add edit note feature, commit 2. Push dist.

* Add edit note feature, commit 3. Switch routing & payload strategy.

* Add edit note feature, commit 4. Fix request payload bug.

* Add edit note feature, commit 5. Bundle prior bug fix for dist.

* Add edit note feature, commit 6. Fix full width style bug.

* Add edit note feature, commit 7. Fix README function signature for editNote().
  • Loading branch information
steven-fox committed Aug 18, 2023
1 parent 6aa9d60 commit e4a362a
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 53 deletions.
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This [Laravel Nova](https://nova.laravel.com) package adds a notes field to Nova
- Notes field on Detail view
- Differentiation between user-added and system-added notes
- Ability to add notes through the UI or programmatically
- Ability to edit user-made notes
- Ability to delete user-made notes (w/ confirmation modal)
- Customizable placeholder support
- Set ability to hide or show the 'Add Note' button
Expand Down Expand Up @@ -84,6 +85,34 @@ To add notes programmatically, use the method provided by the `HasNotes` trait:
public function addNote($note, $user = true, $system = true)
```

## Editing notes programmatically

To edit notes programmatically, use the `editNote` method provided by the `HasNotes` trait:

```php
/**
* Edit a note with the given ID and text.
*
* @param int|string $noteId The ID of the note to edit.
* @param string $text The note text which can contain raw HTML.
* @return \Outl1ne\NovaNotesField\Models\Note
**/
public function editNote($noteId, $text)
```

_Alternatively, you can simply update an existing Note record that's already in memory via standard Eloquent methods:_
```php
$note = $notable->notes()->where('id', '=', $noteId)->first();

$note->update([
'text' => $noteText,
]);

// Or...
$note->text = $noteText;
$note->save();
```

## Configuration

### Publish configuration
Expand All @@ -105,11 +134,11 @@ The available configuration option(s):
| full_width_inputs | boolean | Optionally force all notes fields to display in full width. |
| display_order | string | Optionally set the sort order for notes. Default is `DESC`. |

## Custom delete authorization
## Custom edit & delete authorization

By default, only the user that wrote the note can delete it and no one can delete system notes.
By default, only the user that wrote the note can edit/delete it and no one can edit/delete system notes.

You can define which user(s) can delete which notes by defining a new Laravel authorization Gate called `delete-nova-note`.
You can define which user(s) can edit/delete which notes by defining a new Laravel authorization Gate called `edit-nova-note` and `delete-note-note` respectively.

In your `AuthServiceProvider.php` add a Gate definition like so:

Expand All @@ -121,8 +150,12 @@ use Outl1ne\NovaNotesField\Models\Note;

public function boot()
{
Gate::define('edit-nova-note', function ($user, Note $note) {
// Do whatever here to add custom edit authorization logic, ie:
return $note->created_by === $user->id || $user->isAdmin;
});
Gate::define('delete-nova-note', function ($user, Note $note) {
// Do whatever here, ie:
// Do whatever here to add custom delete authorization logic, ie:
return $note->created_by === $user->id || $user->isAdmin;
});
}
Expand Down
2 changes: 1 addition & 1 deletion dist/js/entry.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"novaNotesField.defaultPlaceholder": "Write note",
"novaNotesField.addNote": "Add note",
"novaNotesField.showMoreNotes": "Show all notes (:hiddenNoteCount more)",
"novaNotesField.edit": "Edit",
"novaNotesField.updateNote": "Update",
"novaNotesField.delete": "Delete",
"novaNotesField.deleteConfirmation": "Are you sure you want to delete this note?",
"novaNotesField.cancel": "Cancel",
Expand Down
135 changes: 90 additions & 45 deletions resources/js/components/Note.vue
Original file line number Diff line number Diff line change
@@ -1,63 +1,108 @@
<template>
<div
class="o1-w-3/5 o1-bg-white dark:o1-bg-slate-800 o1-rounded-md o1-border o1-border-gray-200 dark:o1-border-gray-700 o1-px-2 o1-py-2 o1-flex o1-mb-2 o1-mt-2"
:class="{ 'w-full': fullWidth, 'w-3/5': !fullWidth }"
>
<div class="o1-rounded-lg o1-w-12 o1-h-12 o1-mr-3 o1-overflow-hidden o1-text-center" style="flex-shrink: 0">
<!-- Image -->
<div
v-if="note.system"
class="o1-w-12 o1-h-12 o1-text-sm o1-font-bold o1-bg-gray-50 o1-text-gray-700 dark:o1-text-gray-400"
style="line-height: 3rem"
>
{{ __('novaNotesField.systemUserAbbreviation') }}
</div>
<img class="o1-w-12 o1-h-12" v-else-if="note.created_by_avatar_url" :src="note.created_by_avatar_url" alt="" />
<div
v-else
class="o1-w-12 o1-h-12 o1-text-sm o1-font-bold o1-bg-gray-50 o1-text-gray-700 dark:o1-text-gray-400"
style="line-height: 3rem"
>
{{ !!note.created_by_name ? (note.created_by_name || '').substr(0, 3).toUpperCase() : '??' }}
</div>
</div>

<div>
<!-- Title area -->
<div class="o1-mb-2">
<span class="o1-font-bold o1-text-base o1-text-gray-700 o1-mr-2 dark:o1-text-gray-400">
{{ note.created_by_name ? note.created_by_name : __('novaNotesField.systemUserName') }}
</span>

<span class="o1-text-xs o1-text-gray-700 o1-mr-2 dark:o1-text-gray-400">
{{ formattedCreatedAtDate }}{{ note.system ? ` [${__('novaNotesField.systemUserName')}]` : '' }}
</span>

<span
v-if="!note.system && note.can_delete"
class="o1-text-xs hover:o1-underline o1-cursor-pointer"
style="color: #e74c3c"
@click="$emit('onDeleteRequested', note)"
<template v-if="isEditing">
<NoteInput
v-model.trim="editedText"
@onSubmit="editNote"
:loading="loading"
:fullWidth="fullWidth"
:trixEnabled="trixEnabled"
:editing="true"
/>
</template>
<template v-else>
<div
class="o1-bg-white dark:o1-bg-slate-800 o1-rounded-md o1-border o1-border-gray-200 dark:o1-border-gray-700 o1-px-2 o1-py-2 o1-flex o1-mb-2 o1-mt-2"
:class="{ 'w-full': fullWidth, 'o1-w-3/5': !fullWidth }"
>
<div class="o1-rounded-lg o1-w-12 o1-h-12 o1-mr-3 o1-overflow-hidden o1-text-center" style="flex-shrink: 0">
<!-- Image -->
<div
v-if="note.system"
class="o1-w-12 o1-h-12 o1-text-sm o1-font-bold o1-bg-gray-50 o1-text-gray-700 dark:o1-text-gray-400"
style="line-height: 3rem"
>
[{{ __('novaNotesField.delete') }}]
</span>
{{ __('novaNotesField.systemUserAbbreviation') }}
</div>
<img class="o1-w-12 o1-h-12" v-else-if="note.created_by_avatar_url" :src="note.created_by_avatar_url" alt="" />
<div
v-else
class="o1-w-12 o1-h-12 o1-text-sm o1-font-bold o1-bg-gray-50 o1-text-gray-700 dark:o1-text-gray-400"
style="line-height: 3rem"
>
{{ !!note.created_by_name ? (note.created_by_name || '').substr(0, 3).toUpperCase() : '??' }}
</div>
</div>

<!-- Content -->
<p v-html="note.text" class="o1-whitespace-pre-wrap o1-text-gray-800 dark:o1-text-gray-400"></p>
<div>
<!-- Title area -->
<div class="o1-mb-2">
<span class="o1-font-bold o1-text-base o1-text-gray-700 o1-mr-2 dark:o1-text-gray-400">
{{ note.created_by_name ? note.created_by_name : __('novaNotesField.systemUserName') }}
</span>

<span class="o1-text-xs o1-text-gray-700 o1-mr-2 dark:o1-text-gray-400">
{{ formattedCreatedAtDate }}{{ note.system ? ` [${__('novaNotesField.systemUserName')}]` : '' }}
</span>

<span
v-if="!note.system && note.can_edit"
class="o1-text-xs hover:o1-underline o1-cursor-pointer o1-text-primary-400 o1-mr-2"
@click="onEditRequested"
>[{{ __('novaNotesField.edit') }}]</span
>
<span
v-if="!note.system && note.can_delete"
class="o1-text-xs hover:o1-underline o1-cursor-pointer"
style="color: #e74c3c"
@click="$emit('onDeleteRequested', note)"
>[{{ __('novaNotesField.delete') }}]</span
>
</div>

<!-- Content -->
<p v-html="note.text" class="o1-whitespace-pre-wrap o1-text-gray-800 dark:o1-text-gray-400"></p>
</div>
</div>
</div>
</template>
</template>

<script>
import NoteInput from './NoteInput';
import { format } from 'date-fns';
export default {
props: ['note', 'dateFormat', 'fullWidth'],
components: { NoteInput },
props: ['note', 'dateFormat', 'fullWidth', 'trixEnabled'],
data: () => ({
isEditing: false,
editedText: '',
loading: false,
}),
computed: {
formattedCreatedAtDate() {
return format(new Date(this.note.created_at), this.dateFormat);
},
},
methods: {
onEditRequested() {
this.editedText = this.note.text;
this.isEditing = true;
},
async editNote() {
this.loading = true;
try {
await Nova.request().patch(`/nova-vendor/nova-notes/notes/${this.note.id}`, {
note: this.editedText,
});
this.isEditing = false;
this.$emit('noteEdited', { note: this.note, editedText: this.editedText });
} catch (e) {
Nova.error('Unknown error when trying to edit the note.');
}
this.loading = false;
},
},
};
</script>
4 changes: 2 additions & 2 deletions resources/js/components/NoteInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@
type="button"
:disabled="loading || !modelValue"
>
{{ __('novaNotesField.addNote') }}
{{ editing ? __('novaNotesField.updateNote') : __('novaNotesField.addNote') }}
</DefaultButton>
</div>
</div>
</template>

<script>
export default {
props: ['placeholder', 'modelValue', 'loading', 'trixEnabled', 'fullWidth'],
props: ['placeholder', 'modelValue', 'loading', 'trixEnabled', 'fullWidth', 'editing'],
methods: {
onEnter(e) {
if (e.shiftKey) return true;
Expand Down
6 changes: 6 additions & 0 deletions resources/js/components/NotesField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
:note="note"
:key="note.id"
:date-format="dateFormat"
:trixEnabled="trixEnabled"
@noteEdited="onNoteEdited"
@onDeleteRequested="onNoteDeleteRequested"
/>

Expand Down Expand Up @@ -123,6 +125,10 @@ export default {
this.showDeleteConfirmation = false;
this.loading = false;
},
onNoteEdited({ note, editedText }) {
note.text = editedText;
this.fetchNotes();
},
onNoteDeleteRequested(note) {
this.showDeleteConfirmation = true;
this.noteToDelete = note;
Expand Down
1 change: 1 addition & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
Route::get('/notes', 'NotesController@getNotes');
Route::post('/notes', 'NotesController@addNote');
Route::delete('/notes', 'NotesController@deleteNote');
Route::patch('/notes/{note}', 'NotesController@editNote');
17 changes: 17 additions & 0 deletions src/Http/Controllers/NotesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Laravel\Nova\Nova;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Outl1ne\NovaNotesField\Models\Note;

class NotesController extends Controller
{
Expand Down Expand Up @@ -41,6 +42,22 @@ public function addNote(Request $request)
return response('', 204);
}

// PATCH /notes/{note}
public function editNote(Request $request, Note $note)
{
$noteText = $request->input('note');

if (empty($noteText)) return response(['errors' => ['note' => 'required']], 400);

if (!$note->can_edit) return response()->json(['error' => 'unauthorized'], 400);

$note->update([
'text' => $noteText,
]);

return response('', 204);
}

// DELETE /notes
public function deleteNote(Request $request)
{
Expand Down
20 changes: 19 additions & 1 deletion src/Models/Note.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Note extends Model
protected $casts = ['system' => 'bool'];
protected $fillable = ['model_id', 'model_type', 'text', 'created_by', 'system', 'notable_type', 'notable_id'];
protected $hidden = ['createdBy', 'notable_type', 'notable_id'];
protected $appends = ['created_by_avatar_url', 'created_by_name', 'can_delete'];
protected $appends = ['created_by_avatar_url', 'created_by_name', 'can_delete', 'can_edit'];

public function __construct(array $attributes = [])
{
Expand Down Expand Up @@ -76,4 +76,22 @@ public function getCanDeleteAttribute()

return $user->id === $createdBy->id;
}

public function getCanEditAttribute()
{
if (Gate::has('edit-nova-note')) return Gate::check('edit-nova-note', $this);

if (config()->get('nova.guard')) {
$user = Auth::guard(config('nova.guard'))->user();
} else {
$user = Auth::user();
}

if (empty($user)) return false;

$createdBy = $this->createdBy;
if (empty($createdBy)) return false;

return $user->id === $createdBy->id;
}
}
16 changes: 16 additions & 0 deletions src/Traits/HasNotes.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ public function addNote($note, $user = true, $system = true)
]);
}

/**
* Edit a note with the given ID and text.
*
* @param int|string $noteId The ID of the note to edit.
* @param string $text The note text which can contain raw HTML.
* @return \Outl1ne\NovaNotesField\Models\Note
**/
public function editNote($noteId, $text)
{
$note = $this->notes()->where('id', '=', $noteId)->firstOrFail();
$note->update([
'text' => $text,
]);
return $note;
}

/**
* Deletes a note with given ID and dissociates it from the model.
*
Expand Down

0 comments on commit e4a362a

Please sign in to comment.