Skip to content

Commit

Permalink
Added User attributes (#587)
Browse files Browse the repository at this point in the history
Co-authored-by: Ramin Haeri Azad <[email protected]>
  • Loading branch information
kazoompa and Ramin Haeri Azad authored Jan 12, 2025
1 parent 4e219fe commit 8c88aba
Show file tree
Hide file tree
Showing 8 changed files with 459 additions and 7 deletions.
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

0 comments on commit 8c88aba

Please sign in to comment.