※はじめからはこちら
前回で個別のカードを開いたときの動きを作りました。今回はカード情報の更新などを実装していきます。
カード更新・削除サービスの実装
まずは、カードの部分更新を行うサービスと削除のサービスを実装します。
@@ -84,3 +84,25 @@ def get_card_by_card_id(card_id):
:return:
"""
return Card.get_by_id(card_id)
+
+
+def update_card(card_id, title=None, content=None):
+ """
+ :param int card_id:
+ :param str title:
+ :param str content:
+ :return:
+ :rtype Card:
+ """
+ card = Card.get_by_id(card_id)
+ if title:
+ card.title = title
+ if content:
+ card.content = content
+ card.save()
+ return card
+
+
+def delete_card(card_id):
+ card = Card.get_by_id(card_id)
+ card.delete()
viewへの組み込み
作ったサービスを組み込みます。
@@ -82,3 +82,29 @@ class CardGetApi(View):
'updated_at': card.updated_at,
}
})
+
+ def patch(self, request, board_id, card_id):
+ """
+ カードの内容を更新する
+ """
+ data = json.loads(request.body)
+ title = data.get('title')
+ content = data.get('content')
+ card = kanban_sv.update_card(card_id=card_id, title=title, content=content)
+
+ return JsonResponse({
+ 'card_data': {
+ 'title': card.title,
+ 'content': card.content,
+ 'updated_at': card.updated_at,
+ }
+ })
+
+ def delete(self, _, board_id, card_id):
+ """
+ カードを削除する
+ """
+ kanban_sv.delete_card(card_id=card_id)
+ return JsonResponse({
+ 'success': True
+ })
更新時は更新部分をpatch
メソッドで受け取り、削除時はたんにdelete
メソッドで呼び出します。
Clientへの組み込み
URLは前回割り当ててるのでそのまま/boards/:boardId/cards/:cardId
で使えます。メソッドだけ変えてClientに組み込みます。
@@ -71,6 +71,23 @@ class KanbanClient extends Client {
const response = await this._get(`${this.baseUrl}/boards/${boardId}/cards/${cardId}/`);
return response.data.cardData;
}
+
+ async updateCardData({
+ boardId,
+ cardId,
+ content,
+ title,
+ }) {
+ const response = await this._patch(`${this.baseUrl}/boards/${boardId}/cards/${cardId}/`, {
+ content,
+ title,
+ });
+ return response.data.cardData;
+ }
+
+ async deleteCard({ boardId, cardId }) {
+ await this._delete(`${this.baseUrl}/boards/${boardId}/cards/${cardId}/`);
+ }
}
Viewに追加したpatch
とdelete
を呼び出すようにしています。
コンポーネント・Storeへの組み込み
コンポーネントから処理を実行できるようにしていきます。まずはStoreに組み込みます。
@@ -51,6 +51,32 @@ const actions = {
const cardData = await kanbanClient.getCardData({ boardId, cardId });
commit('setFocusedCard', cardData);
},
+ async updateCardContent({ commit }, { boardId, cardId, content }) {
+ const cardData = await kanbanClient.updateCardData({
+ boardId,
+ cardId,
+ content,
+ });
+ commit('setFocusedCard', cardData);
+ },
+ async updateCardTitle({ commit, dispatch }, { boardId, cardId, title }) {
+ const cardData = await kanbanClient.updateCardData({
+ boardId,
+ cardId,
+ title,
+ });
+ commit('setFocusedCard', cardData);
+ // titleはボード自体に出ているので他のクライアントへの反映を依頼する必要がある
+ dispatch('broadcastBoardData');
+ },
+ async deleteCard({ dispatch }, { boardId, cardId }) {
+ await kanbanClient.deleteCard({
+ boardId,
+ cardId,
+ });
+ // カード自体はボード自体に出ているので他のクライアントへの反映を依頼する必要がある
+ dispatch('broadcastBoardData');
+ },
};
ポイントは更新関連のActionがupdateCardContent
とupdateCardTitle
の2つあるところです。どちらも更新処理自体はClientに追加したupdateCardData
を使うのですが、タイトルはボードの画面にも出ているので更新したら他のクライアントにも通知しなければいけません。
そこで、Titleの更新だけは更新完了後にdispatch('broadcastBoardData');
を呼び出して他のクライアントにボードデータの再取得を促しています。
では、これをコンポーネントに組み込みます。
@@ -1,23 +1,32 @@
<template>
- <div class="modal" aria-labelledby="modal-title" aria-hidden="true">
- <div class="modal-dialog" role="document">
+ <div v-if="fetchFocusedCard" class="modal" aria-labelledby="modal-title" aria-hidden="true" @click="close">
+ <div class="modal-dialog" role="document" @click.prevent.stop="">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modal-title">
- <span>
+ <span v-show="!isTitleEditing" @dblclick="startTitleEdit">
{{ focusedCard.title }}
</span>
+ <span v-show="isTitleEditing">
+ <input type="text" v-model="editTitle">
+ <button type="button" class="btn btn-primary" @click="saveTitle">save</button>
+ </span>
</h5>
- <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="close">
<span aria-hidden="true">×</span>
</button>
</div>
- <div class="modal-body">
- <p class="card-content">{{ focusedCard.content }}</p>
+ <div class="modal-body" v-show="!isContentEditing">
+ <p v-if="focusedCard.content" @dblclick="startContentEdit" class="card-content">{{ focusedCard.content }}</p>
+ <p v-else @click="startContentEdit" class="empty-content">enter content.</p>
+ </div>
+ <div class="modal-body" v-show="isContentEditing">
+ <textarea class="edit-area" v-model="editContent"></textarea>
+ <button type="button" class="btn btn-primary" @click="saveContent">Save</button>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-danger">delete</button>
- <button type="button" class="btn btn-primary">Close</button>
+ <button type="button" class="btn btn-danger" @click="deleteCardAction">delete</button>
+ <button type="button" class="btn btn-primary" @click="close">Close</button>
</div>
</div>
</div>
@@ -31,7 +40,6 @@ const { mapState, mapActions } = createNamespacedHelpers('board');
export default {
- name: 'CardShow',
props: {
cardId: {
type: Number,
@@ -42,12 +50,64 @@ export default {
default: null,
},
},
+ name: 'CardShow',
computed: {
...mapState(['focusedCard']),
},
+ data() {
+ return {
+ isContentEditing: false,
+ editContent: '',
+ isTitleEditing: false,
+ editTitle: '',
+ };
+ },
methods: {
+ close() {
+ this.$router.push({
+ path: `/boards/${this.boardId}`,
+ query: this.$route.query,
+ });
+ },
+ async deleteCardAction() {
+ await this.deleteCard({
+ boardId: this.boardId,
+ cardId: this.cardId,
+ });
+ window.alert('delete succeeded');
+ this.close();
+ },
+ startContentEdit() {
+ this.isContentEditing = true;
+ this.editContent = this.focusedCard.content;
+ },
+ startTitleEdit() {
+ this.isTitleEditing = true;
+ this.editTitle = this.focusedCard.title;
+ },
+ async saveContent() {
+ this.isContentEditing = false;
+ if (this.editContent === this.focusedCard.content) return;
+ await this.updateCardContent({
+ boardId: this.boardId,
+ cardId: this.cardId,
+ content: this.editContent,
+ });
+ },
+ async saveTitle() {
+ this.isTitleEditing = false;
+ if (this.editTitle === this.focusedCard.title) return;
+ await this.updateCardTitle({
+ boardId: this.boardId,
+ cardId: this.cardId,
+ title: this.editTitle,
+ });
+ },
...mapActions([
'fetchFocusedCard',
+ 'updateCardContent',
+ 'updateCardTitle',
+ 'deleteCard',
]),
},
watch: {
StoreのActionを追加するとともに、タイトルやコンテンツをダブルルクリックすると編集画面になるように変更しました。
地味ですが、タイトル編集時には後ろにでているカード一覧のほうのタイトルも変更されていることがわかります。もちろん他のブラウザで同じボードをみていても同時に変更されています。
これでカードデータの更新・削除が実装できました。
次回
リストの追加・削除・更新を作っていきます。