Basic goal list editing / creation / deletion

This commit is contained in:
Lordmau5 2023-11-20 17:50:43 +01:00
parent 40f2750f4f
commit b5add9b8a0
5 changed files with 216 additions and 142 deletions

View File

@ -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) {

View File

@ -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>

View File

@ -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;

View File

@ -63,7 +63,11 @@ export default class BingoGame {
names.push(goal.name);
}
});
names.push(subgroup.name);
});
names.push(group.name);
});
return names;

View File

@ -6,6 +6,8 @@ import {
export default class BingoGoalList {
name: string;
description: string = '';
@Type(() => BingoGoal)
goals: BingoGoal[] = [];