Basic goal list editing / creation / deletion
This commit is contained in:
parent
40f2750f4f
commit
b5add9b8a0
@ -8,6 +8,8 @@
|
||||
<q-card-section class="q-gutter-y-md column">
|
||||
<q-input
|
||||
v-model="goal_name"
|
||||
:error="!!name_error.length"
|
||||
:error-message="name_error"
|
||||
square
|
||||
filled
|
||||
counter
|
||||
@ -155,9 +157,21 @@ goal_name.value = data.goal?.name ?? '';
|
||||
goal_description.value = data.goal?.description ?? '';
|
||||
goal_tags.value = data.goal?.tags ?? [];
|
||||
|
||||
const name_error = computed(() => {
|
||||
if (isNameReserved(goal_name.value)) {
|
||||
return 'Name is reserved';
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
function isNameReserved(name: string) {
|
||||
return name.length && data.reserved_names.some(reserved_name =>
|
||||
reserved_name !== data.goal?.name
|
||||
&& stringCompare(reserved_name, name));
|
||||
}
|
||||
|
||||
function canSave() {
|
||||
return goal_name.value?.length > 0
|
||||
&& !data.reserved_names.some(name => name !== data.goal?.name && stringCompare(name, goal_name.value));
|
||||
return !isNameReserved(goal_name.value);
|
||||
}
|
||||
|
||||
function addTag(value: string, done: Function) {
|
||||
|
@ -7,7 +7,9 @@
|
||||
>
|
||||
<q-card-section class="q-gutter-y-md column">
|
||||
<q-input
|
||||
v-model="goal_name"
|
||||
v-model="goal_list_name"
|
||||
:error="!!name_error.length"
|
||||
:error-message="name_error"
|
||||
square
|
||||
filled
|
||||
counter
|
||||
@ -19,7 +21,7 @@
|
||||
|
||||
<q-input
|
||||
class="col-10"
|
||||
v-model="goal_description"
|
||||
v-model="goal_list_description"
|
||||
square
|
||||
filled
|
||||
counter
|
||||
@ -38,36 +40,24 @@
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-select
|
||||
label="Select Tags"
|
||||
square
|
||||
filled
|
||||
v-model="goal_tags"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
clearable
|
||||
hint="Optional"
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
:options="all_tags"
|
||||
@new-value="addTag"
|
||||
></q-select>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-actions>
|
||||
<q-btn
|
||||
v-if="data.goal"
|
||||
v-if="data.goal_list"
|
||||
flat
|
||||
color="red"
|
||||
icon="delete"
|
||||
:label="delete_label"
|
||||
@click="deleteGoal()"
|
||||
:disabled="!delete_enabled"
|
||||
/>
|
||||
@click="deleteGoalList()"
|
||||
:disabled="!delete_actually_enabled"
|
||||
>
|
||||
<q-tooltip v-if="has_goals">
|
||||
Cannot delete goal list with goals
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-space/>
|
||||
|
||||
@ -83,7 +73,7 @@
|
||||
color="green"
|
||||
icon="save"
|
||||
label="Save"
|
||||
@click="saveGoal()"
|
||||
@click="saveGoalList()"
|
||||
:disabled="!canSave()"
|
||||
/>
|
||||
</q-card-actions>
|
||||
@ -95,43 +85,50 @@
|
||||
bordered
|
||||
>
|
||||
<q-card-section>
|
||||
<MarkdownRenderer :text="goal_description"/>
|
||||
<MarkdownRenderer :text="goal_list_description"/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BingoGoal from '@/js/lib/BingoGoal.ts';
|
||||
import BingoGoalList from '@/js/lib/BingoGoalList.ts';
|
||||
import {
|
||||
stringCompare
|
||||
} from '@/js/lib/Util.ts';
|
||||
|
||||
const emit = defineEmits([
|
||||
'cancel',
|
||||
'createGoal',
|
||||
'updateGoal',
|
||||
'deleteGoal'
|
||||
'createGoalList',
|
||||
'updateGoalList',
|
||||
'deleteGoalList'
|
||||
]);
|
||||
|
||||
const {
|
||||
data
|
||||
} = defineProps<{
|
||||
data: {
|
||||
all_tags: string[],
|
||||
reserved_names: string[],
|
||||
goal?: BingoGoal,
|
||||
goal_list?: BingoGoalList,
|
||||
},
|
||||
}>();
|
||||
|
||||
const all_tags: Ref<string[]> = ref([ ...data.all_tags ]);
|
||||
|
||||
const markdown_preview = ref(false);
|
||||
|
||||
const delete_label = ref('Delete (5)');
|
||||
const delete_enabled = ref(false);
|
||||
|
||||
const has_goals = computed(() => {
|
||||
return data.goal_list?.goals?.length;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (has_goals) {
|
||||
delete_label.value = 'Delete';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let countdown = 5;
|
||||
let interval = setInterval(() => {
|
||||
if (--countdown <= 0) {
|
||||
@ -147,58 +144,55 @@ onMounted(() => {
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
const goal_name = ref('');
|
||||
const goal_description = ref('');
|
||||
const goal_tags: Ref<string[]> = ref([ ]);
|
||||
const delete_actually_enabled = computed(() => {
|
||||
return delete_enabled.value && !has_goals;
|
||||
});
|
||||
|
||||
goal_name.value = data.goal?.name ?? '';
|
||||
goal_description.value = data.goal?.description ?? '';
|
||||
goal_tags.value = data.goal?.tags ?? [];
|
||||
const goal_list_name = ref('');
|
||||
const goal_list_description = ref('');
|
||||
|
||||
function canSave() {
|
||||
return goal_name.value?.length > 0
|
||||
&& !data.reserved_names.some(name => name !== data.goal?.name && stringCompare(name, goal_name.value));
|
||||
goal_list_name.value = data.goal_list?.name ?? '';
|
||||
goal_list_description.value = data.goal_list?.description ?? '';
|
||||
|
||||
const name_error = computed(() => {
|
||||
if (isNameReserved(goal_list_name.value)) {
|
||||
return 'Name is reserved';
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
function isNameReserved(name: string) {
|
||||
return name.length && data.reserved_names.some(reserved_name =>
|
||||
reserved_name !== data.goal_list?.name
|
||||
&& stringCompare(reserved_name, name));
|
||||
}
|
||||
|
||||
function addTag(value: string, done: Function) {
|
||||
// Exit if tag already exists
|
||||
if (goal_tags.value.some(tag => stringCompare(tag, value))) {
|
||||
done();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = all_tags.value.find(tag => stringCompare(tag, value));
|
||||
value = tag ?? value;
|
||||
|
||||
if (!data.all_tags.some(tag => stringCompare(tag, value))) {
|
||||
all_tags.value.push(value);
|
||||
}
|
||||
|
||||
done(value, 'toggle');
|
||||
function canSave() {
|
||||
return !isNameReserved(goal_list_name.value);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
function saveGoal() {
|
||||
const goal = new BingoGoal(goal_name.value);
|
||||
goal.description = goal_description.value;
|
||||
goal.tags = goal_tags.value;
|
||||
function saveGoalList() {
|
||||
const goal_list = new BingoGoalList(goal_list_name.value);
|
||||
goal_list.description = goal_list_description.value;
|
||||
|
||||
// Create new goal
|
||||
if (!data.goal) {
|
||||
emit('createGoal', goal);
|
||||
// Create new goal list
|
||||
if (!data.goal_list) {
|
||||
emit('createGoalList', goal_list);
|
||||
}
|
||||
// Update existing goal
|
||||
// Update existing goal list
|
||||
else {
|
||||
emit('updateGoal', data.goal, goal);
|
||||
goal_list.goals = data.goal_list.goals;
|
||||
|
||||
emit('updateGoalList', data.goal_list, goal_list);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteGoal() {
|
||||
emit('deleteGoal', data.goal);
|
||||
function deleteGoalList() {
|
||||
emit('deleteGoalList', data.goal_list);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<!-- TODO: Can't seem to work with flexbox to an extent that makes me happy... -->
|
||||
|
||||
<q-card class="column" style="width: 100%; min-width: 700px; height: 80vh;">
|
||||
<q-card class="column" style="width: 100%; min-width: 800px; height: 80vh;">
|
||||
<div class="col-2">
|
||||
<q-card-section class="row justify-evenly q-gutter-md">
|
||||
<q-input
|
||||
@ -86,16 +86,16 @@
|
||||
/>
|
||||
<div>{{ prop.node.item?.name || prop.node.label }}</div>
|
||||
<q-btn
|
||||
v-if="prop.node.goal?.description?.length"
|
||||
v-if="prop.node.item?.description?.length"
|
||||
icon="help_outline"
|
||||
flat
|
||||
round
|
||||
size="sm"
|
||||
@click="openInfoDialog(prop.node.goal.description)"
|
||||
@click="event => openInfoDialog(prop.node.item?.description, event)"
|
||||
></q-btn>
|
||||
<q-badge
|
||||
outline
|
||||
v-for="tag in prop.node.goal.tags"
|
||||
v-for="tag in prop.node.item?.tags"
|
||||
:key="tag"
|
||||
:color="getColorForString(tag)"
|
||||
class="q-ml-sm"
|
||||
@ -116,6 +116,14 @@
|
||||
@click="event => openEditGoalListDialog(prop.node, event)"
|
||||
/>
|
||||
<div>{{ prop.node.item?.name || prop.node.label }}</div>
|
||||
<q-btn
|
||||
v-if="prop.node.item?.description?.length"
|
||||
icon="help_outline"
|
||||
flat
|
||||
round
|
||||
size="sm"
|
||||
@click="event => openInfoDialog(prop.node.item?.description, event)"
|
||||
></q-btn>
|
||||
<q-badge
|
||||
outline
|
||||
color="orange"
|
||||
@ -220,57 +228,64 @@
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<q-dialog
|
||||
v-model="description_dialog"
|
||||
style="min-width: 700px; max-width: 80vw; min-height: 10vh; max-height: 80vh;"
|
||||
>
|
||||
<q-dialog v-model="description_dialog">
|
||||
<q-card
|
||||
class="column"
|
||||
flat
|
||||
bordered
|
||||
class="full-width"
|
||||
style="min-width: 700px; max-width: 80vw; min-height: 10vh; max-height: 70vh;"
|
||||
>
|
||||
<q-card-section class="full-height">
|
||||
<q-input
|
||||
v-model="game_description"
|
||||
class="full-height"
|
||||
square
|
||||
filled
|
||||
counter
|
||||
autogrow
|
||||
maxlength="500"
|
||||
label="Description"
|
||||
type="text"
|
||||
>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
<div class="col-1">
|
||||
<q-card-section>
|
||||
<span class="text-h4">Description Editor</span>
|
||||
</q-card-section>
|
||||
</div>
|
||||
|
||||
<q-card-actions>
|
||||
<q-btn
|
||||
flat
|
||||
color="red"
|
||||
icon="cancel"
|
||||
label="Preview"
|
||||
:loading="loading"
|
||||
@click="openInfoDialog(game_description)"
|
||||
/>
|
||||
<div class="col-6">
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model="game_description"
|
||||
square
|
||||
filled
|
||||
counter
|
||||
autogrow
|
||||
maxlength="500"
|
||||
label="Description"
|
||||
type="text"
|
||||
/>
|
||||
</q-card-section>
|
||||
</div>
|
||||
|
||||
<q-space/>
|
||||
<div class="col-1">
|
||||
<q-card-actions class="full-height items-end">
|
||||
<q-btn
|
||||
flat
|
||||
color="red"
|
||||
icon="cancel"
|
||||
label="Preview"
|
||||
:loading="loading"
|
||||
@click="event => openInfoDialog(game_description, event)"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
color="red"
|
||||
icon="cancel"
|
||||
label="Save"
|
||||
:loading="loading"
|
||||
/>
|
||||
</q-card-actions>
|
||||
<q-space/>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
color="red"
|
||||
icon="cancel"
|
||||
label="Save"
|
||||
:loading="loading"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="info_dialog" style="min-width: 700px; max-width: 80vw; min-height: 10vh; max-height: 80vh;">
|
||||
<q-dialog v-model="info_dialog">
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
style="min-width: 700px; max-width: 80vw; min-height: 10vh; max-height: 80vh;"
|
||||
>
|
||||
<q-card-section>
|
||||
<MarkdownRenderer :text="info_dialog_text"/>
|
||||
@ -288,24 +303,26 @@
|
||||
/>
|
||||
</q-dialog>
|
||||
|
||||
<!-- <q-dialog v-model="edit_goal_list_dialog" persistent>
|
||||
<q-dialog v-model="edit_goal_list_dialog" persistent>
|
||||
<EditGoalListDialog
|
||||
:data="edit_goal_list_data"
|
||||
@cancel="edit_goal_list_dialog = false"
|
||||
@create-goal-list="create_goal_list"
|
||||
@update-goal-list="update_goal_list"
|
||||
@delete-goal-list="delete_goal_list"
|
||||
@emit-goal-list="emit_goal_list"
|
||||
/>
|
||||
</q-dialog> -->
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Node {
|
||||
label: string;
|
||||
item?: BingoGoal | BingoGoalList | BingoCategory;
|
||||
header?: string; // 'goal' | 'goal-list' | 'category' | 'add-goal' | 'add-goal-list' | 'add-category'
|
||||
children?: Node[];
|
||||
goal?: BingoGoal;
|
||||
parent?: BingoCategory | BingoGoalList;
|
||||
|
||||
label: string;
|
||||
header?: string; // 'goal' | 'goal-list' | 'category' | 'add-goal' | 'add-goal-list' | 'add-category'
|
||||
|
||||
children?: Node[];
|
||||
}
|
||||
|
||||
import {
|
||||
@ -338,7 +355,7 @@ game.value.is_local = true;
|
||||
const game_id = ref(game.value.id);
|
||||
const game_name = ref(game.value.name);
|
||||
const short_description = ref(game.value.short_description);
|
||||
const game_description = ref(game.value.description);
|
||||
const game_description = ref(game.value.description.substring(0, 500));
|
||||
const description_dialog = ref(false);
|
||||
|
||||
const expanded = ref<Node[]>([]);
|
||||
@ -373,7 +390,10 @@ function saveGame() {
|
||||
const info_dialog = ref(false);
|
||||
const info_dialog_text = ref('');
|
||||
|
||||
function openInfoDialog(text: string) {
|
||||
function openInfoDialog(text: string, event: Event) {
|
||||
// Stop expanding of the tree
|
||||
event.stopPropagation();
|
||||
|
||||
info_dialog_text.value = text;
|
||||
info_dialog.value = true;
|
||||
}
|
||||
@ -395,7 +415,7 @@ const edit_goal_data = ref<EditGoalData>({
|
||||
});
|
||||
const edit_goal_dialog = ref(false);
|
||||
const openEditGoalDialog = (node: Node) => {
|
||||
edit_goal_data.value.goal = node.goal;
|
||||
edit_goal_data.value.goal = node.item as BingoGoal;
|
||||
edit_goal_data.value.reserved_names = game.value?.reserved_names ?? [];
|
||||
edit_goal_data.value.parent_group = node.parent;
|
||||
edit_goal_data.value.all_tags = game.value?.getAllTags() ?? [];
|
||||
@ -457,10 +477,12 @@ const delete_goal = (goal: BingoGoal) => {
|
||||
/* Edit Goal List Dialog */
|
||||
interface EditGoalListData {
|
||||
goal_list?: BingoGoalList;
|
||||
reserved_names: string[];
|
||||
parent_group: BingoCategory | undefined;
|
||||
}
|
||||
|
||||
const edit_goal_list_data = ref<EditGoalListData>({
|
||||
reserved_names: [] as string[],
|
||||
parent_group: undefined
|
||||
});
|
||||
|
||||
@ -471,27 +493,51 @@ function openEditGoalListDialog(node: Node, event: Event) {
|
||||
event.stopPropagation();
|
||||
|
||||
edit_goal_list_data.value.goal_list = node.item as BingoGoalList;
|
||||
edit_goal_list_data.value.reserved_names = game.value?.reserved_names ?? [];
|
||||
edit_goal_list_data.value.parent_group = node.parent as BingoCategory;
|
||||
edit_goal_list_dialog.value = true;
|
||||
}
|
||||
|
||||
const emit_goal_list = (goal_list: BingoGoalList) => {
|
||||
const create_goal_list = (goal_list: BingoGoalList) => {
|
||||
// We always assume parent group is a category. You can't add goal lists to goal lists.
|
||||
const category = edit_goal_list_data.value.parent_group as BingoCategory;
|
||||
|
||||
// Add new goal list
|
||||
if (!edit_goal_list_data.value.goal_list) {
|
||||
// Add goal list to category
|
||||
if (category) {
|
||||
category.goal_lists.push(goal_list);
|
||||
}
|
||||
// Replace existing goal list
|
||||
// Add goal list to game
|
||||
else {
|
||||
const index = category.goal_lists.findIndex(g => g === goal_list);
|
||||
category.goal_lists.splice(index, 1, goal_list);
|
||||
game.value?.items.push(goal_list);
|
||||
}
|
||||
|
||||
// Reset values
|
||||
edit_goal_list_data.value = {
|
||||
parent_group: undefined
|
||||
parent_group: undefined,
|
||||
reserved_names: []
|
||||
};
|
||||
edit_goal_list_dialog.value = false;
|
||||
};
|
||||
|
||||
const update_goal_list = (old_goal_list: BingoGoalList, goal_list: BingoGoalList) => {
|
||||
// We always assume parent group is a category. You can't add goal lists to goal lists.
|
||||
const category = edit_goal_list_data.value.parent_group as BingoCategory;
|
||||
|
||||
// Replace existing goal list in category
|
||||
if (category) {
|
||||
const index = category.goal_lists.findIndex(g => g === old_goal_list);
|
||||
category.goal_lists.splice(index, 1, goal_list);
|
||||
}
|
||||
// Replace existing goal list in game
|
||||
else {
|
||||
const index = game.value?.items.findIndex(g => g === old_goal_list);
|
||||
game.value?.items.splice(index, 1, goal_list);
|
||||
}
|
||||
|
||||
// Reset values
|
||||
edit_goal_list_data.value = {
|
||||
parent_group: undefined,
|
||||
reserved_names: []
|
||||
};
|
||||
edit_goal_list_dialog.value = false;
|
||||
};
|
||||
@ -502,13 +548,22 @@ const delete_goal_list = (goal_list: BingoGoalList) => {
|
||||
// We always assume parent group is a category. You can't add goal lists to goal lists.
|
||||
const category = edit_goal_list_data.value.parent_group as BingoCategory;
|
||||
|
||||
const index = category.goal_lists.findIndex(g => g === goal_list);
|
||||
category.goal_lists.splice(index, 1);
|
||||
// Delete goal list from category
|
||||
if (category) {
|
||||
const index = category.goal_lists.findIndex(g => g === goal_list);
|
||||
category.goal_lists.splice(index, 1);
|
||||
}
|
||||
// Delete goal list from game
|
||||
else {
|
||||
const index = game.value?.items.findIndex(g => g === goal_list);
|
||||
game.value?.items.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset values
|
||||
edit_goal_list_data.value = {
|
||||
parent_group: undefined
|
||||
parent_group: undefined,
|
||||
reserved_names: []
|
||||
};
|
||||
edit_goal_list_dialog.value = false;
|
||||
};
|
||||
@ -575,32 +630,37 @@ const nodes = computed(() => {
|
||||
const is_goal_list = item instanceof BingoGoalList;
|
||||
|
||||
const group_node: Node = {
|
||||
label: item.name,
|
||||
item,
|
||||
|
||||
label: item.name,
|
||||
header: is_category
|
||||
? 'category'
|
||||
: is_goal_list
|
||||
? 'goal-list'
|
||||
: '',
|
||||
|
||||
children: []
|
||||
};
|
||||
|
||||
if (is_category) {
|
||||
group_node.children = item.goal_lists.map(goal_list => {
|
||||
const goal_list_node: Node = {
|
||||
label: goal_list.name,
|
||||
item: goal_list,
|
||||
parent: item,
|
||||
|
||||
label: goal_list.name,
|
||||
header: 'goal-list',
|
||||
|
||||
children: []
|
||||
};
|
||||
|
||||
goal_list_node.children?.push(...goal_list.goals.map(goal => {
|
||||
const goal_node: Node = {
|
||||
label: goal.name,
|
||||
item: goal,
|
||||
header: 'goal',
|
||||
goal,
|
||||
parent: goal_list
|
||||
parent: goal_list,
|
||||
|
||||
label: goal.name,
|
||||
header: 'goal'
|
||||
};
|
||||
|
||||
return goal_node;
|
||||
@ -624,11 +684,11 @@ const nodes = computed(() => {
|
||||
else if (is_goal_list) {
|
||||
group_node.children = item.goals.map(goal => {
|
||||
const goal_node: Node = {
|
||||
label: goal.name,
|
||||
item: goal,
|
||||
header: 'goal',
|
||||
goal,
|
||||
parent: item
|
||||
parent: item,
|
||||
|
||||
label: goal.name,
|
||||
header: 'goal'
|
||||
};
|
||||
|
||||
return goal_node;
|
||||
|
@ -63,7 +63,11 @@ export default class BingoGame {
|
||||
names.push(goal.name);
|
||||
}
|
||||
});
|
||||
|
||||
names.push(subgroup.name);
|
||||
});
|
||||
|
||||
names.push(group.name);
|
||||
});
|
||||
|
||||
return names;
|
||||
|
@ -6,6 +6,8 @@ import {
|
||||
export default class BingoGoalList {
|
||||
name: string;
|
||||
|
||||
description: string = '';
|
||||
|
||||
@Type(() => BingoGoal)
|
||||
goals: BingoGoal[] = [];
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user