Feature/move label editor (#1069)

* update default color

* move labels editor
This commit is contained in:
Hayden 2022-03-19 11:31:17 -08:00 committed by GitHub
parent 8c0c8be659
commit 8f569509bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 212 additions and 281 deletions

View file

@ -29,7 +29,6 @@ export default defineComponent({
Based on -> https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
*/
const ACCESSIBILITY_THRESHOLD = 0.179;
function pickTextColorBasedOnBgColorAdvanced(bgColor: string, lightColor: string, darkColor: string) {
@ -53,4 +52,4 @@ export default defineComponent({
};
},
});
</script>
</script>

View file

@ -24,11 +24,11 @@
value: 'new',
to: '/group/data/units',
},
// {
// text: 'Labels',
// value: 'new',
// to: '/group/data/labels',
// },
{
text: 'Labels',
value: 'new',
to: '/group/data/labels',
},
]"
>
</BaseOverflowButton>

View file

@ -27,7 +27,7 @@
<!-- Delete Dialog -->
<BaseDialog
v-model="deleteDialog"
:title="$tc('general.delete')"
:title="$tc('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
@confirm="deleteFood"

View file

@ -0,0 +1,198 @@
<template>
<div>
<!-- Create New Dialog -->
<BaseDialog v-model="state.createDialog" title="New Label" :icon="$globals.icons.tags" @submit="createLabel">
<v-card-text>
<MultiPurposeLabel :label="createLabelData" />
<div class="mt-4">
<v-text-field v-model="createLabelData.name" :label="$t('general.name')"> </v-text-field>
<InputColor v-model="createLabelData.color" />
</div>
</v-card-text>
</BaseDialog>
<!-- Edit Dialog -->
<BaseDialog
v-model="state.editDialog"
:icon="$globals.icons.tags"
title="Edit Label"
:submit-text="$tc('general.save')"
@submit="editSaveLabel"
>
<v-card-text v-if="editLabel">
<MultiPurposeLabel :label="editLabel" />
<div class="mt-4">
<v-text-field v-model="editLabel.name" :label="$t('general.name')"> </v-text-field>
<InputColor v-model="editLabel.color" />
</div>
</v-card-text>
</BaseDialog>
<!-- Delete Dialog -->
<BaseDialog
v-model="state.deleteDialog"
:title="$tc('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
@confirm="deleteLabel"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
</v-card-text>
</BaseDialog>
<!-- Recipe Data Table -->
<BaseCardSectionTitle :icon="$globals.icons.tags" section title="Labels"> </BaseCardSectionTitle>
<CrudTable
:table-config="tableConfig"
:headers.sync="tableHeaders"
:data="labels"
:bulk-actions="[]"
@delete-one="deleteEventHandler"
@edit-one="editEventHandler"
>
<template #button-row>
<BaseButton create @click="state.createDialog = true">
<template #icon> {{ $globals.icons.tags }} </template>
Create
</BaseButton>
</template>
<template #item.name="{ item }">
<MultiPurposeLabel v-if="item" :label="item">
{{ item.name }}
</MultiPurposeLabel>
</template>
</CrudTable>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from "@nuxtjs/composition-api";
import { validators } from "~/composables/use-validators";
import { useUserApi } from "~/composables/api";
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
import { MultiPurposeLabelSummary } from "~/types/api-types/labels";
export default defineComponent({
components: { MultiPurposeLabel },
setup() {
const userApi = useUserApi();
const tableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders = [
{
text: "Id",
value: "id",
show: false,
},
{
text: "Name",
value: "name",
show: true,
},
];
const state = reactive({
createDialog: false,
editDialog: false,
deleteDialog: false,
});
// ============================================================
// Labels
const labels = ref([] as MultiPurposeLabelSummary[]);
async function refreshLabels() {
const { data } = await userApi.multiPurposeLabels.getAll();
labels.value = data ?? [];
}
// Create
const createLabelData = ref({
groupId: "",
id: "",
name: "",
color: "",
});
async function createLabel() {
await userApi.multiPurposeLabels.createOne(createLabelData.value);
createLabelData.value = {
groupId: "",
id: "",
name: "",
color: "",
};
refreshLabels();
state.createDialog = false;
}
// Delete
const deleteTarget = ref<MultiPurposeLabelSummary | null>(null);
function deleteEventHandler(item: MultiPurposeLabelSummary) {
state.deleteDialog = true;
deleteTarget.value = item;
}
async function deleteLabel() {
if (!deleteTarget.value) {
return;
}
const { data } = await userApi.multiPurposeLabels.deleteOne(deleteTarget.value.id);
if (data) {
refreshLabels();
}
state.deleteDialog = false;
}
// Edit
const editLabel = ref<MultiPurposeLabelSummary | null>(null);
function editEventHandler(item: MultiPurposeLabelSummary) {
state.editDialog = true;
editLabel.value = item;
if (!editLabel.value.color) {
editLabel.value.color = "#E0E0E0";
}
}
async function editSaveLabel() {
if (!editLabel.value) {
return;
}
const { data } = await userApi.multiPurposeLabels.updateOne(editLabel.value.id, editLabel.value);
if (data) {
refreshLabels();
}
state.editDialog = false;
}
refreshLabels();
return {
state,
tableConfig,
tableHeaders,
labels,
validators,
deleteEventHandler,
deleteLabel,
editLabel,
editEventHandler,
editSaveLabel,
createLabel,
createLabelData,
};
},
});
</script>

View file

@ -21,7 +21,7 @@
<!-- Delete Dialog -->
<BaseDialog
v-model="deleteDialog"
:title="$tc('general.delete')"
:title="$tc('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
@confirm="deleteFood"

View file

@ -178,7 +178,7 @@
<v-lazy>
<div class="d-flex justify-end mt-10">
<ButtonLink to="/shopping-lists/labels" text="Manage Labels" :icon="$globals.icons.tags" />
<ButtonLink to="/group/data/labels" text="Manage Labels" :icon="$globals.icons.tags" />
</div>
</v-lazy>
</v-container>

View file

@ -1,12 +1,12 @@
<template>
<v-container v-if="shoppingLists" class="narrow-container">
<BaseDialog v-model="createDialog" :title="$t('shopping-list.create-shopping-list')" @submit="createOne">
<BaseDialog v-model="createDialog" :title="$tc('shopping-list.create-shopping-list')" @submit="createOne">
<v-card-text>
<v-text-field v-model="createName" autofocus :label="$t('shopping-list.new-list')"> </v-text-field>
</v-card-text>
</BaseDialog>
<BaseDialog v-model="deleteDialog" :title="$t('general.confirm')" color="error" @confirm="deleteOne">
<BaseDialog v-model="deleteDialog" :title="$tc('general.confirm')" color="error" @confirm="deleteOne">
<v-card-text> Are you sure you want to delete this item?</v-card-text>
</BaseDialog>
<BasePageTitle divider>
@ -33,11 +33,11 @@
</v-card>
</section>
<div class="d-flex justify-end mt-10">
<ButtonLink to="/shopping-lists/labels" text="Manage Labels" :icon="$globals.icons.tags" />
<ButtonLink to="/group/data/labels" text="Manage Labels" :icon="$globals.icons.tags" />
</div>
</v-container>
</template>
<script lang="ts">
import { defineComponent, useAsync, reactive, toRefs } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";

View file

@ -1,266 +0,0 @@
<template>
<v-container class="narrow-container">
<BaseDialog v-model="createDialog" title="New Label" :icon="$globals.icons.tags" @submit="createLabel">
<v-card-text>
<v-text-field v-model="createLabelData.name" :label="$t('general.name')"> </v-text-field>
</v-card-text>
</BaseDialog>
<BaseDialog
v-model="deleteDialog"
:title="$t('general.confirm')"
:icon="$globals.icons.alert"
color="error"
@confirm="confirmDelete"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
</v-card-text>
</BaseDialog>
<BasePageTitle divider>
<template #header>
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/shopping-cart.svg')"></v-img>
</template>
<template #title> Shopping Lists Labels </template>
</BasePageTitle>
<BaseButton create @click="createDialog = true" />
<section v-if="labels" class="mt-4">
<v-text-field v-model="searchInput" :label="$t('sidebar.search')" clearable>
<template #prepend>
<v-icon>{{ $globals.icons.search }}</v-icon>
</template>
</v-text-field>
<v-sheet v-for="(label, index) in results" :key="label.id">
<div class="d-flex px-2 py-2 pt-3">
<MultiPurposeLabel :label="label" />
<div class="ml-auto">
<v-btn v-if="!isOpen[label.id]" class="mx-1" icon @click.prevent="deleteLabel(label.id)">
<v-icon>
{{ $globals.icons.delete }}
</v-icon>
</v-btn>
<v-btn v-if="!isOpen[label.id]" class="mx-1" icon @click="toggleIsOpen(label)">
<v-icon>
{{ $globals.icons.edit }}
</v-icon>
</v-btn>
</div>
</div>
<v-card-text v-if="isOpen[label.id]">
<div class="d-md-flex" style="gap: 30px">
<v-text-field v-model="labels[index].name" :label="$t('general.name')"> </v-text-field>
<div style="max-width: 300px">
<InputColor v-model="labels[index].color" />
</div>
</div>
<div class="d-flex justify-end">
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.delete,
text: 'Delete',
event: 'delete',
},
{
icon: $globals.icons.close,
text: 'Cancel',
event: 'cancel',
},
{
icon: $globals.icons.save,
text: 'Save',
event: 'save',
},
]"
@cancel="resetToLastGoodValue(label, index)"
@save="updateLabel(label)"
@delete="deleteLabel(label.id)"
/>
</div>
</v-card-text>
<v-divider></v-divider>
</v-sheet>
</section>
</v-container>
</template>
<script lang="ts">
import { defineComponent, ref, useAsync, computed } from "@nuxtjs/composition-api";
import Fuse from "fuse.js";
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
import { MultiPurposeLabelSummary } from "~/types/api-types/labels";
export default defineComponent({
components: { MultiPurposeLabel },
setup() {
// ==========================================================
// API Operations
const api = useUserApi();
const deleteDialog = ref(false);
const deleteTargetId = ref("");
async function confirmDelete() {
await api.multiPurposeLabels.deleteOne(deleteTargetId.value);
refreshLabels();
deleteTargetId.value = "";
}
function deleteLabel(itemId: string) {
deleteTargetId.value = itemId;
deleteDialog.value = true;
}
const createDialog = ref(false);
const createLabelData = ref({
name: "",
color: "",
});
async function createLabel() {
createLabelData.value.color = getRandomHex();
const { data } = await api.multiPurposeLabels.createOne(createLabelData.value);
if (data) {
refreshLabels();
}
}
async function updateLabel(label: MultiPurposeLabelSummary) {
const { data } = await api.multiPurposeLabels.updateOne(label.id, label);
if (data) {
refreshLabels();
toggleIsOpen(label);
}
}
const labels = useAsync(async () => {
const { data } = await api.multiPurposeLabels.getAll();
return data;
}, useAsyncKey());
async function refreshLabels() {
const { data } = await api.multiPurposeLabels.getAll();
labels.value = data ?? [];
}
// ==========================================================
// Component Helpers
const lastGoodValue = ref<{ [key: string]: MultiPurposeLabelSummary }>({});
function saveLastGoodValue(label: MultiPurposeLabelSummary) {
lastGoodValue.value[label.id] = { ...label };
}
function resetToLastGoodValue(label: MultiPurposeLabelSummary, index: number) {
const lgv = lastGoodValue.value[label.id];
if (lgv && labels.value) {
labels.value[index] = lgv;
labels.value = [...labels.value];
}
toggleIsOpen(label);
}
const isOpen = ref<{ [key: string]: boolean }>({});
function toggleIsOpen(label: MultiPurposeLabelSummary) {
isOpen.value[label.id] = !isOpen.value[label.id];
if (isOpen.value[label.id]) {
saveLastGoodValue(label);
}
isOpen.value = { ...isOpen.value };
}
// ==========================================================
// Color Generators
function getRandomHex() {
const letters = "BCDEF".split("");
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * letters.length)];
}
return color;
}
function setRandomHex(labelIndex: number) {
if (!labels.value) {
return;
}
labels.value[labelIndex].color = getRandomHex();
labels.value = [...labels.value];
}
// ==========================================================
// Search / Filter
const searchInput = ref("");
const labelNames = computed(() => {
return labels.value?.map((label) => label.name) ?? [];
});
const fuseOpts = {
shouldSort: true,
threshold: 0.5,
location: 0,
distance: 100,
findAllMatches: true,
maxPatternLength: 32,
minMatchCharLength: 2,
keys: ["name"],
};
const fuse = computed(() => {
return new Fuse(labelNames.value, fuseOpts);
});
const results = computed(() => {
if (!searchInput.value) {
return labels.value;
}
const foundName = fuse.value.search(searchInput.value).map((result) => result.item);
return labels.value?.filter((label) => foundName.includes(label.name)) ?? [];
});
return {
saveLastGoodValue,
resetToLastGoodValue,
deleteDialog,
deleteTargetId,
confirmDelete,
createLabelData,
createLabel,
createDialog,
results,
searchInput,
updateLabel,
deleteLabel,
setRandomHex,
toggleIsOpen,
isOpen,
labels,
refreshLabels,
};
},
head: {
title: "Shopping List Labels",
},
});
</script>

View file

@ -6,7 +6,7 @@ from pydantic import UUID4
class MultiPurposeLabelCreate(CamelModel):
name: str
color: str = ""
color: str = "#E0E0E0"
class MultiPurposeLabelSave(MultiPurposeLabelCreate):