Add actions

This commit is contained in:
rijkvanzanten
2020-07-23 10:48:56 -04:00
parent f4ac7421e5
commit 3bbe53685e
12 changed files with 573 additions and 24 deletions

View File

@@ -11,9 +11,13 @@ export default defineInterface(({ i18n }) => ({
{
field: 'includeSeconds',
name: i18n.t('include_seconds'),
width: 'half',
interface: 'toggle',
default_value: false,
system: {
width: 'half',
interface: 'toggle',
},
database: {
default_value: false,
},
},
],
}));

View File

@@ -6,7 +6,8 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('many_to_one'),
icon: 'arrow_right_alt',
component: InterfaceManyToOne,
types: ['string', 'text', 'integer', 'bigInteger'],
types: ['uuid', 'string', 'text', 'integer', 'bigInteger'],
relationship: 'm2o',
options: [
{
field: 'template',

View File

@@ -7,8 +7,9 @@ export type InterfaceConfig = {
icon: string;
name: string | VueI18n.TranslateResult;
component: Component;
options: Partial<Field>[] | Component;
options: DeepPartial<Field>[] | Component;
types: Type[];
relationship?: null | 'm2o' | 'o2m' | 'm2m';
hideLabel?: boolean;
hideLoader?: boolean;
};

View File

@@ -26,9 +26,42 @@
"duplicate_where_to": "Where would you like to duplicate this field to?",
"editing_field": "Editing {field}",
"within_collectoin": "within {collection}",
"schema_setup_title": "How should this field save to the database?",
"key": "Key",
"alias": "Alias",
"bigInteger": "Big Integer",
"boolean": "Boolean",
"date": "Date",
"datetime": "DateTime",
"decimal": "Decimal",
"float": "Float",
"integer": "Integer",
"json": "JSON",
"string": "String",
"text": "Text",
"time": "Time",
"timestamp": "Timestamp",
"binary": "Binary",
"uuid": "UUID",
"unknown": "Unknown",
"include_seconds": "Include Seconds",
"add_note": "Add a helpful note for users...",
"default_value": "Default Value",
"add_a_default_value": "Add a default value...",
"allow_null": "Allow NULL",
"allow_null_label": "Can be set to \"NULL\"",
"interface_setup_title": "How will users view or interact with this field?",
"field_setup_title": "What type of field are you creating?",
"relationship_setup_title": "What kind of relationship are you creating?",
"interface_setup_title": "How will users view or interact with this field?",
"display_setup_title": "How should this fields value be displayed?",
"schema_options_title": "All set! Below are some optional configuration options...",
"creating_field": "Creating New Field",
@@ -48,8 +81,6 @@
"database_column_name": "Database Column Name",
"translations": "Translations",
"note": "Note",
"add_helpful_note": "Enter a helpful comment for App users...",
"default_value": "Default Value",
"enter_a_value": "Enter a value...",
"length": "Length",
"required": "Required",
@@ -151,7 +182,6 @@
"interfaces": "Interfaces",
"interface_count": "No Interfaces | One Interface | {count} Interfaces",
"datetime": "DateTime",
"today": "Today",
"yesterday": "Yesterday",
@@ -163,7 +193,6 @@
"date-fns_date_short": "MMM d, u",
"date-fns_date_short_no_year": "MMM d",
"month": "Month",
"date": "Date",
"year": "Year",
"select_all": "Select All",
@@ -225,7 +254,6 @@
"primary_key_field": "Primary Key Field",
"type": "Type",
"number": "Number",
"string": "String",
"creating_new_collection": "Creating New Collection",
"status": "Status",
"sort": "Sort",
@@ -660,7 +688,6 @@
"activity_log": "Activity Log",
"add_field_filter": "Add a field filter",
"add_new": "Add New",
"add_note": "Add a helpful note for users...",
"additional_info": "Additional Info",
"admin_email": "Admin Email",
"admin_password": "Admin Password",
@@ -1026,7 +1053,6 @@
"spacing": "Spacing",
"statuses": "Statuses",
"template": "Template",
"text": "Text",
"translation": "Translation",
"translated_field_name": "Translated field name...",
"not_translated_in_language": "Not Translated in {language}",

View File

@@ -0,0 +1,100 @@
<template>
<div class="actions">
<v-button secondary :to="`/settings/data-model/${collection}`">
{{ $t('cancel') }}
</v-button>
<div class="spacer" />
<v-button @click="previousTab" secondary :disabled="previousDisabled">
{{ $t('previous') }}
</v-button>
<v-button v-if="currentTabIndex < tabs.length - 1" @click="nextTab" :disabled="nextDisabled">
{{ $t('next') }}
</v-button>
<v-button v-else @click="$emit('save')" :loading="saving">
{{ $t('save') }}
</v-button>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from '@vue/composition-api';
import useSync from '@/composables/use-sync';
type Tab = {
text: string;
value: string;
disabled: boolean;
};
export default defineComponent({
props: {
collection: {
type: String,
required: true,
},
current: {
type: Array,
required: true,
},
tabs: {
type: Array as PropType<Tab[]>,
required: true,
},
saving: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const _currentTab = useSync(props, 'current', emit);
const currentTabIndex = computed(() => props.tabs.findIndex((tab) => tab.value === props.current[0]));
const previousDisabled = computed(() => {
return currentTabIndex.value === 0;
});
const nextDisabled = computed(() => {
const nextTab = props.tabs[currentTabIndex.value + 1];
if (nextTab) {
return nextTab.disabled;
}
return true;
});
return { _currentTab, previousDisabled, previousTab, nextDisabled, nextTab, currentTabIndex };
function previousTab() {
const previousTab = props.tabs[currentTabIndex.value - 1];
if (previousTab) {
_currentTab.value = [previousTab.value];
}
}
function nextTab() {
const nextTab = props.tabs[currentTabIndex.value + 1];
if (nextTab) {
_currentTab.value = [nextTab.value];
}
}
},
});
</script>
<style lang="scss" scoped>
.actions {
display: contents;
}
.spacer {
flex-grow: 1;
}
.v-button:not(:last-child) {
margin-right: 8px;
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<div>
<h2 class="type-title">{{ $t('display_setup_title') }}</h2>
<v-fancy-select class="select" :items="selectItems" v-model="_field.system.display" />
<template v-if="_field.system.display">
<v-form
v-if="
selectedDisplay.options &&
Array.isArray(selectedDisplay.options) &&
selectedDisplay.options.length > 0
"
:fields="selectedDisplay.options"
primary-key="+"
v-model="_field.system.options"
/>
<v-notice v-else>
{{ $t('no_options_available') }}
</v-notice>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import displays from '@/displays';
import useSync from '@/composables/use-sync';
export default defineComponent({
props: {
fieldData: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
},
setup(props, { emit }) {
const _field = useSync(props, 'fieldData', emit);
const availabledisplays = computed(() =>
displays.filter((display) => {
const matchesType = display.types.includes(props.fieldData.database.type);
const matchesRelation = true;
// if (props.type === 'standard') {
// matchesRelation = display.relationship === null || display.relationship === undefined;
// } else if (props.type === 'file') {
// matchesRelation = display.relationship === 'm2o';
// } else if (props.type === 'files') {
// matchesRelation = display.relationship === 'm2m';
// } else {
// matchesRelation = display.relationship === props.type;
// }
return matchesType && matchesRelation;
})
);
const selectItems = computed(() =>
availabledisplays.value.map((display) => ({
text: display.name,
value: display.id,
icon: display.icon,
}))
);
const selectedDisplay = computed(() => {
return displays.find((display) => display.id === _field.value.system.display);
});
return { _field, selectItems, selectedDisplay };
},
});
</script>
<style lang="scss" scoped>
.type-title,
.select {
margin-bottom: 32px;
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<div>
<h2 class="type-title">{{ $t('interface_setup_title') }}</h2>
<v-fancy-select class="select" :items="selectItems" v-model="_field.system.interface" />
<template v-if="_field.system.interface">
<v-form
v-if="
selectedInterface.options &&
Array.isArray(selectedInterface.options) &&
selectedInterface.options.length > 0
"
:fields="selectedInterface.options"
primary-key="+"
v-model="_field.system.options"
/>
<v-notice v-else>
{{ $t('no_options_available') }}
</v-notice>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import interfaces from '@/interfaces';
import useSync from '@/composables/use-sync';
export default defineComponent({
props: {
fieldData: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
},
setup(props, { emit }) {
const _field = useSync(props, 'fieldData', emit);
const availableInterfaces = computed(() =>
interfaces.filter((inter) => {
const matchesType = inter.types.includes(props.fieldData.database.type);
let matchesRelation = false;
if (props.type === 'standard') {
matchesRelation = inter.relationship === null || inter.relationship === undefined;
} else if (props.type === 'file') {
matchesRelation = inter.relationship === 'm2o';
} else if (props.type === 'files') {
matchesRelation = inter.relationship === 'm2m';
} else {
matchesRelation = inter.relationship === props.type;
}
return matchesType && matchesRelation;
})
);
const selectItems = computed(() =>
availableInterfaces.value.map((inter) => ({
text: inter.name,
value: inter.id,
icon: inter.icon,
}))
);
const selectedInterface = computed(() => {
return interfaces.find((inter) => inter.id === _field.value.system.interface);
});
return { _field, selectItems, selectedInterface };
},
});
</script>
<style lang="scss" scoped>
.type-title,
.select {
margin-bottom: 32px;
}
</style>

View File

@@ -0,0 +1,9 @@
<template>
<div>Relationship</div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
export default defineComponent({});
</script>

View File

@@ -0,0 +1,125 @@
<template>
<div>
<h2 class="type-title">{{ $t('schema_setup_title') }}</h2>
<div class="form">
<div class="field">
<div class="label type-label">{{ $t('key') }}</div>
<v-input autofocus class="monospace" v-model="_field.field" db-safe />
</div>
<div class="field">
<div class="label type-label">{{ $t('type') }}</div>
<v-select :value="_field.database.type" @input="setType" :items="typesWithLabels" />
</div>
<div class="field full">
<div class="label type-label">{{ $t('note') }}</div>
<v-input v-model="_field.database.comment" :placeholder="$t('add_note')" />
</div>
<div class="field">
<div class="label type-label">{{ $t('default_value') }}</div>
<v-input
class="monospace"
v-model="_field.database.default_value"
:placeholder="$t('add_a_default_value')"
/>
</div>
<div class="field">
<div class="label type-label">{{ $t('length') }}</div>
<v-input v-model="_field.database.max_length" />
</div>
<div class="field">
<div class="label type-label">{{ $t('allow_null') }}</div>
<v-checkbox v-model="_field.database.is_nullable" :label="$t('allow_null_label')" block />
</div>
<!--
@todo add unique when the API supports it
<div class="field">
<div class="label type-label">{{ $t('unique') }}</div>
<v-input v-model="_field.database.unique" />
</div> -->
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import useSync from '@/composables/use-sync';
import { types, Type } from '@/stores/fields/types';
import i18n from '@/lang';
export default defineComponent({
props: {
fieldData: {
type: Object,
required: true,
},
},
setup(props, { emit }) {
const _field = useSync(props, 'fieldData', emit);
const typesWithLabels = computed(() =>
types
.filter((type) => {
// Remove alias and unknown, as those aren't real column types you can use
return ['alias', 'unknown'].includes(type) === false;
})
.map((type) => {
return {
value: type,
text: i18n.t(type),
};
})
);
return { _field, typesWithLabels, setType };
function setType(value: Type) {
if (value === 'uuid') {
_field.value.system.special = 'uuid';
} else {
_field.value.system.special = null;
}
// We'll reset the interface/display as they most likely won't work for the newly selected
// type
_field.value.system.interface = null;
_field.value.system.options = null;
_field.value.system.display = null;
_field.value.system.display_options = null;
_field.value.database.type = value;
}
},
});
</script>
<style lang="scss" scoped>
.type-title {
margin-bottom: 32px;
}
.form {
display: grid;
grid-gap: 32px;
grid-template-columns: 1fr 1fr;
}
.full {
grid-column: 1 / span 2;
}
.label {
margin-bottom: 8px;
}
.monospace {
--v-input-font-family: var(--family-monospace);
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<v-tabs vertical v-model="_currentTab">
<v-tab v-for="tab in tabs" :key="tab.value" :value="tab.value" :disabled="tab.disabled">
{{ tab.text }}
</v-tab>
</v-tabs>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
import useSync from '@/composables/use-sync';
export default defineComponent({
props: {
tabs: {
type: Array as PropType<string[]>,
required: true,
},
current: {
type: Array as PropType<string[]>,
default: 'schema',
},
type: {
type: String,
default: 'standard',
},
},
setup(props, { emit }) {
const _currentTab = useSync(props, 'current', emit);
return { _currentTab };
},
});
</script>

View File

@@ -1,14 +1,40 @@
<template>
<v-modal :active="active" title="Test">
{{ field }} {{ type }}
<router-link to="/settings/data-model/customers">Back</router-link>
<v-modal :active="active" title="Test" persistent>
<template #sidebar>
<setup-tabs :current.sync="currentTab" :tabs="tabs" :type="type" />
</template>
<setup-schema v-if="currentTab[0] === 'schema'" :field-data.sync="fieldData" :type="type" />
<setup-relationship v-if="currentTab[0] === 'relationship'" :field-data.sync="fieldData" :type="type" />
<setup-interface v-if="currentTab[0] === 'interface'" :field-data.sync="fieldData" :type="type" />
<setup-display v-if="currentTab[0] === 'display'" :field-data.sync="fieldData" :type="type" />
<template #footer>
<setup-actions :collection="collection" :current.sync="currentTab" :tabs="tabs" />
</template>
</v-modal>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from '@vue/composition-api';
import { defineComponent, onMounted, ref, computed, reactive, PropType } from '@vue/composition-api';
import SetupTabs from './components/tabs.vue';
import SetupActions from './components/actions.vue';
import SetupSchema from './components/schema.vue';
import SetupRelationship from './components/relationship.vue';
import SetupInterface from './components/interface.vue';
import SetupDisplay from './components/display.vue';
import { i18n } from '@/lang';
import { isEmpty } from 'lodash';
export default defineComponent({
components: {
SetupTabs,
SetupActions,
SetupSchema,
SetupRelationship,
SetupInterface,
SetupDisplay,
},
props: {
collection: {
type: String,
@@ -19,23 +45,74 @@ export default defineComponent({
required: true,
},
type: {
type: String,
type: String as PropType<'standard' | 'file' | 'files' | 'm2o' | 'o2m' | 'm2m'>,
default: null,
},
},
setup() {
setup(props) {
const active = ref(false);
const fieldData = reactive({
collection: props.collection,
field: null,
database: {
type: null,
default_value: null,
max_length: null,
is_nullable: true,
comment: '',
},
system: {
hidden: false,
interface: null,
options: null,
display: null,
display_options: null,
readonly: false,
special: null,
},
});
// This makes sure we still see the enter animation
onMounted(() => {
active.value = true;
});
return { active };
const tabs = computed(() => {
return [
{
text: i18n.t('schema'),
value: 'schema',
disabled: false,
},
// {
// text: i18n.t('relationship'),
// value: 'relationship',
// },
{
text: i18n.t('interface'),
value: 'interface',
disabled: interfaceDisabled(),
},
{
text: i18n.t('display'),
value: 'display',
disabled: displayDisabled(),
},
];
});
const currentTab = ref(['schema']);
return { active, tabs, currentTab, fieldData };
function interfaceDisabled() {
return isEmpty(fieldData.field) || isEmpty(fieldData.database.type);
}
function displayDisabled() {
return isEmpty(fieldData.field) || isEmpty(fieldData.database.type);
}
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -24,6 +24,7 @@ export type Type =
| 'json'
| 'uuid'
| 'binary'
| 'uuid'
| 'unknown';
export const types: Type[] = [
@@ -41,6 +42,7 @@ export const types: Type[] = [
'time',
'timestamp',
'binary',
'uuid',
'unknown',
];