Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new user attributes UI #587

Merged
merged 1 commit into from
Jan 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions agate-ui/src/components/HtmlAnchorHint.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<div v-html="htmlText" @click="onClick"></div>
</template>

<script lang="ts">
defineComponent({
name: 'HtmlAnchorHint',
});
</script>

<script setup lang="ts">
interface Props {
trKey: string;
text: string;
url: string;
}

const props = defineProps<Props>();
const { t } = useI18n();
const htmlText = computed(() => t(props.trKey, { url: `<a href="${props.url}" target="_blank">${props.text}</a>` }));

function onClick() {
window.open(props.url, '_blank');
}

// onMounted(() => {
// htmlText.value = t(props.trKey, { url: `<a href="${props.url}" target="_blank">${props.text}</a>` });
// });
</script>
6 changes: 0 additions & 6 deletions agate-ui/src/components/SystemProperties.vue
Original file line number Diff line number Diff line change
Expand Up @@ -134,18 +134,12 @@

<script setup lang="ts">
import SystemPropertiesDialog from 'src/components/SystemPropertiesDialog.vue';

const systemStore = useSystemStore();
const { t } = useI18n();

const showDialog = ref(false);

const config = computed(() => systemStore.configuration);

onMounted(() => {
systemStore.init();
});

const onEdit = () => {
showDialog.value = true;
};
Expand Down
180 changes: 180 additions & 0 deletions agate-ui/src/components/SystemUserAttributes.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<template>
<div>
<div class="text-h6 q-mb-md">
{{ t('system.attributes.title') }}
</div>

<html-anchor-hint
class="text-help"
trKey="system.attributes.hint"
:text="t('translations').toLocaleLowerCase()"
url="/admin/translations"
/>

<q-table
:rows="attributes"
flat
row-key="name"
:columns="columns"
:pagination="initialPagination"
:hide-pagination="attributes.length <= initialPagination.rowsPerPage"
>
<template v-slot:top-left>
<q-btn size="sm" icon="add" color="primary" :label="t('add')" @click="onAdd" />
</template>
<template v-slot:top-right>
<q-input v-model="filter" debounce="300" :placeholder="t('search')" dense clearable class="q-mr-md">
<template v-slot:prepend>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body="props">
<q-tr :props="props" @mouseover="onOverRow(props.row)" @mouseleave="onLeaveRow(props.row)">
<q-td key="name" :props="props">
<span class="text-primary">{{ props.row.name }}</span>
<div class="float-right">
<q-btn
rounded
dense
flat
size="sm"
color="secondary"
:icon="toolsVisible[props.row.name] ? 'edit' : 'none'"
:title="t('edit')"
class="q-ml-xs"
@click="onShowEdit(props.row)"
/>
<q-btn
rounded
dense
flat
size="sm"
color="secondary"
:title="t('delete')"
:icon="toolsVisible[props.row.name] ? 'delete' : 'none'"
class="q-ml-xs"
@click="onShowDelete(props.row)"
/>
</div>
</q-td>
<q-td key="type" :props="props">
<span>{{ props.row.type }}</span>
</q-td>
<q-td key="description" :props="props">
<span>{{ props.row.description }}</span>
</q-td>
<q-td key="values" :props="props" @mouseover="onOverRow(props.row)" @mouseleave="onLeaveRow(props.row)">
<q-chip class="q-ml-none" v-for="(value, index) in props.row.values" :key="index">
{{ value }}
</q-chip>
</q-td>
<q-td key="required" :props="props">
<q-icon :name="props.row.required ? 'check_box' : 'check_box_outline_blank'" size="sm" dense />
</q-td>
</q-tr> </template
>/
</q-table>

<confirm-dialog
v-model="showDelete"
:title="t('system.attributes.remove')"
:text="t('system.attributes.remove_confirm', { name: selected?.name })"
@confirm="onDelete"
/>

<system-user-attributes-dialog
v-model="showEdit"
:attribute="selected"
@saved="onSavedAttribute"
@cancel="onCancel"
/>
</div>
</template>

<script lang="ts">
export default defineComponent({
name: 'SystemUserAttributes',
});
</script>

<script setup lang="ts">
import type { AttributeConfigurationDto } from 'src/models/Agate';
import { DefaultAlignment } from 'src/components/models';
import HtmlAnchorHint from 'src/components/HtmlAnchorHint.vue';
import ConfirmDialog from 'src/components/ConfirmDialog.vue';
import SystemUserAttributesDialog from 'src/components/SystemUserAttributesDialog.vue';

const systemStore = useSystemStore();
const { t } = useI18n();

// const showDialog = ref(false);
const toolsVisible = ref<{ [key: string]: boolean }>({});
const initialPagination = ref({
descending: false,
page: 1,
rowsPerPage: 20,
});
const attributes = computed(
() =>
systemStore.userAttributes?.filter((attr) =>
filter.value ? attr.name.toLowerCase().includes(filter.value.toLowerCase()) : true,
) || [],
);
const filter = ref('');
const selected = ref();
const showEdit = ref(false);
const showDelete = ref(false);

const columns = computed(() => [
{ name: 'name', label: t('name'), field: 'name', align: DefaultAlignment },
{ name: 'type', label: t('type'), field: 'type', align: DefaultAlignment },
{ name: 'description', label: t('description'), field: 'description', align: DefaultAlignment },
{
name: 'values',
label: t('values'),
field: 'values',
format: (val: string) => (val || '').split(/\s*,\s*/),
align: DefaultAlignment,
},
{ name: 'required', label: t('required'), field: 'required', align: DefaultAlignment },
]);

function onOverRow(row: AttributeConfigurationDto) {
toolsVisible.value[row.name] = true;
}

function onLeaveRow(row: AttributeConfigurationDto) {
toolsVisible.value[row.name] = false;
}

function onAdd() {
selected.value = undefined;
showEdit.value = true;
}

function onShowEdit(row: AttributeConfigurationDto) {
selected.value = row;
showEdit.value = true;
}

function onShowDelete(row: AttributeConfigurationDto) {
selected.value = row;
showDelete.value = true;
}

function onDelete() {
if (selected.value) {
systemStore.removeAttribute(selected.value);
}
}

function onCancel() {
showEdit.value = false;
}

function onSavedAttribute() {
showEdit.value = false;
systemStore.init();
}
</script>
164 changes: 164 additions & 0 deletions agate-ui/src/components/SystemUserAttributesDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<template>
<q-dialog v-model="showDialog" persistent @hide="onHide">
<q-card class="dialog-md">
<q-card-section>
<div class="text-h6">{{ editMode ? t('system.attributes.update') : t('system.attributes.add') }}</div>
</q-card-section>

<q-separator />
<q-card-section>
<q-form ref="formRef">
<q-input
v-model="newAttribue.name"
dense
type="text"
:label="t('name') + ' *'"
class="q-mb-md"
lazy-rules
:rules="[validateRequired, validateUnique]"
:disable="editMode"
>
</q-input>
<q-input
v-model="newAttribue.description"
:label="t('description')"
class="q-mb-md"
dense
type="textarea"
lazy-rules
/>
<q-select
v-model="type"
:label="t('type') + ' *'"
:options="typeOptions"
class="q-mb-md"
dense
emit-value
map-options
/>
<q-input
v-model="values"
dense
type="text"
:label="t('values')"
:hint="t('system.attributes.values_hint')"
class="q-mb-md"
:disable="newAttribue.type !== 'STRING'"
>
</q-input>
<q-checkbox
v-model="newAttribue.required"
:label="t('required')"
class="q-mb-md"
dense
/>
</q-form>
</q-card-section>

<q-separator />

<q-card-actions align="right" class="bg-grey-3">
<q-btn flat :label="t('cancel')" color="secondary" @click="onCancel" v-close-popup />
<q-btn flat :label="t('save')" color="primary" @click="onSave" />
</q-card-actions>
</q-card>
</q-dialog>
</template>

<script lang="ts">
export default defineComponent({
name: 'SystemUserAttributesDialog',
});
</script>

<script setup lang="ts">
import type { AttributeConfigurationDto } from 'src/models/Agate';
import { notifyError, notifySuccess } from 'src/utils/notify';

const { t } = useI18n();
const systemStore = useSystemStore();

interface DialogProps {
modelValue: boolean;
attribute: AttributeConfigurationDto | null;
}

const props = defineProps<DialogProps>();
const emit = defineEmits(['update:modelValue', 'saved', 'cancel']);
const formRef = ref();
const showDialog = ref(props.modelValue);
const newAttribue = ref<AttributeConfigurationDto>({} as AttributeConfigurationDto);
const type = computed({
get: () => newAttribue.value.type,
set: (value) => {
newAttribue.value.type = value;
if (value !== 'STRING') {
newAttribue.value.values = [];
}
},
});
const values = computed({
get: () => newAttribue.value.values?.join(', '),
set: (value) => {
newAttribue.value.values = value.split(/\s*,\s*/);
},
})
const editMode = computed(() => !!props.attribute && !!props.attribute.name);
const typeOptions = computed(() =>
['STRING', 'NUMBER', 'BOOLEAN', 'INTEGER'].map((value) => ({ label: t(`system.attributes.types.${value}`), value })),
);

function validateRequired(value: string) {
return !!value || t('name_required');
}

function validateUnique(value: string) {
if (systemStore.userAttributes) {
const exists = systemStore.userAttributes.find((attr) => attr.name === value);
return !exists || t('system.attributes.name_exists');
}
return true;
}

watch(
() => props.modelValue,
(value) => {
if (value) {
if (props.attribute) {
newAttribue.value = { ...props.attribute } as AttributeConfigurationDto;
} else {
newAttribue.value = { type: 'STRING', required: false } as AttributeConfigurationDto;
}
}

showDialog.value = value;
},
);

function onHide() {
emit('update:modelValue', false);
}

function onCancel() {
emit('cancel');
}

async function onSave() {
const valid = await formRef.value.validate();
if (valid) {
const endpoint = editMode.value ? systemStore.updateAttribute : systemStore.addAttribute;
endpoint(newAttribue.value)
.then(() => {
notifySuccess(t('system.attributes.updated'));
emit('saved');
})
.catch(() => {
notifyError(t('system.attributes.update_failed'));
})
.finally(() => {
newAttribue.value = { type: 'STRING' } as AttributeConfigurationDto;
emit('update:modelValue', false);
});
}
}
</script>
Loading