adminforth 1.2.56 → 1.2.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/spa/index.html +1 -1
  2. package/dist/spa/package-lock.json +215 -1
  3. package/dist/spa/package.json +2 -0
  4. package/dist/spa/spa/src/types/AdminForthConfig.ts +1477 -0
  5. package/dist/spa/spa/src/types/FrontendAPI.ts +121 -0
  6. package/dist/spa/src/App.vue +176 -133
  7. package/dist/spa/src/assets/base.css +2 -0
  8. package/dist/spa/src/components/Breadcrumbs.vue +3 -2
  9. package/dist/spa/src/components/CustomDatePicker.vue +6 -4
  10. package/dist/spa/src/components/CustomRangePicker.vue +11 -7
  11. package/dist/spa/src/components/Filters.vue +16 -5
  12. package/dist/spa/src/components/MenuLink.vue +3 -3
  13. package/dist/spa/src/components/ResourceForm.vue +140 -111
  14. package/dist/spa/src/components/ResourceListTable.vue +76 -101
  15. package/dist/spa/src/components/SkeleteLoader.vue +23 -0
  16. package/dist/spa/src/components/Toast.vue +18 -7
  17. package/dist/spa/src/components/ValueRenderer.vue +10 -4
  18. package/dist/spa/src/composables/useStores.ts +25 -11
  19. package/dist/spa/src/index.scss +2 -1
  20. package/dist/spa/src/main.ts +1 -1
  21. package/dist/spa/src/router/index.ts +0 -1
  22. package/dist/spa/src/stores/user.ts +21 -3
  23. package/dist/spa/src/types/AdminForthConfig.ts +1477 -0
  24. package/dist/spa/src/types/FrontendAPI.ts +121 -0
  25. package/dist/spa/src/utils.ts +6 -4
  26. package/dist/spa/src/views/CreateView.vue +2 -9
  27. package/dist/spa/src/views/EditView.vue +1 -9
  28. package/dist/spa/src/views/ListView.vue +17 -16
  29. package/dist/spa/src/views/LoginView.vue +57 -31
  30. package/dist/spa/src/views/ResourceParent.vue +1 -2
  31. package/dist/spa/src/views/ShowView.vue +17 -21
  32. package/dist/spa/tailwind.config.js +3 -1
  33. package/dist/spa/vite.config.ts +7 -0
  34. package/package.json +2 -2
@@ -2,7 +2,7 @@
2
2
  <!-- drawer component -->
3
3
  <div id="drawer-navigation"
4
4
 
5
- class="fixed right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-black"
5
+ class="fixed right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-gray-900"
6
6
 
7
7
  :class="show ? 'top-0 transform-none' : ''"
8
8
  tabindex="-1" aria-labelledby="drawer-navigation-label"
@@ -70,8 +70,8 @@
70
70
 
71
71
  <CustomRangePicker
72
72
  v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
73
- :min="c.min"
74
- :max="c.max"
73
+ :min="getFilterMinValue(c.name)"
74
+ :max="getFilterMaxValue(c.name)"
75
75
  :valueStart="getFilterItem({ column: c, operator: 'gte' })"
76
76
  @update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
77
77
  :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
@@ -189,8 +189,6 @@ function setFilterItem({ column, operator, value }) {
189
189
  if (index !== -1) {
190
190
  filtersStore.filters.splice(index, 1);
191
191
  }
192
- emits('update:filters', [...filtersStore.filters]);
193
- return;
194
192
  } else {
195
193
  if (index === -1) {
196
194
  filtersStore.setFilter({ field: column.name, value, operator });
@@ -207,5 +205,18 @@ function getFilterItem({ column, operator }) {
207
205
 
208
206
  async function clear() {
209
207
  filtersStore.clearFilters();
208
+ emits('update:filters', [...filtersStore.filters]);
209
+ }
210
+
211
+ function getFilterMinValue(columnName) {
212
+ if(props.columnsMinMax && props.columnsMinMax[columnName]) {
213
+ return props.columnsMinMax[columnName]?.min
214
+ }
215
+ }
216
+
217
+ function getFilterMaxValue(columnName) {
218
+ if(props.columnsMinMax && props.columnsMinMax[columnName]) {
219
+ return props.columnsMinMax[columnName]?.max
220
+ }
210
221
  }
211
222
  </script>
@@ -1,16 +1,16 @@
1
1
  <template>
2
2
  <RouterLink
3
3
  :to="{name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
4
- class="flex items-center py-2 text-nav-menu-text rounded-default hover:bg-nav-menu-bg-hover active:bg-nav-menu-bg-active" role="menuitem"
4
+ class="flex group items-center py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarActive dark:active:bg-darkSidebarHover" role="menuitem"
5
5
  :class="{
6
6
  'px-4': isChild,
7
7
  'px-2': !isChild,
8
- 'bg-nav-menu-bg-active dark:bg-gray-700': item.resourceId ?
8
+ 'bg-lightSidebarItemActive dark:bg-darkSidebarItemActive': item.resourceId ?
9
9
  ($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
10
10
  ($route.name === item.path)
11
11
  }"
12
12
  >
13
- <component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" ></component>
13
+ <component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons dark:text-darkSidebarIcons transition duration-75 group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover" ></component>
14
14
  <span class="ms-3">{{ item.label }}</span>
15
15
  </RouterLink>
16
16
  </template>
@@ -1,131 +1,138 @@
1
1
  <template>
2
2
  <div class="rounded-default">
3
-
4
3
  <div
5
- class="relative shadow-resourse-form-shadow sm:rounded-lg dark:shadow-2xl dark:shadow-black rounded-default"
4
+ class="relative shadow-resourseFormShadow dark:shadow-darkResourseFormShadow sm:rounded-lg dark:shadow-2xl rounded-default"
6
5
  >
7
6
  <form autocomplete="off" @submit.prevent>
8
7
  <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 ">
9
- <thead class="text-xs text-gray-700 uppercase bg-form-view-heading dark:bg-gray-700 dark:text-gray-400">
8
+ <thead class="text-xs text-gray-700 uppercase bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 block md:table-row-group">
10
9
  <tr>
11
- <th scope="col" class="px-6 py-3">
10
+ <th scope="col" class="px-6 py-3 hidden md:table-cell">
12
11
  Field
13
12
  </th>
14
- <th scope="col" class="px-6 py-3 w-4/6">
13
+ <th scope="col" class="px-6 py-3 w-5/6 hidden md:table-cell">
15
14
  Value
16
15
  </th>
16
+
17
+
17
18
  </tr>
18
19
  </thead>
19
20
  <tbody>
20
21
  <tr v-for="column, i in editableColumns" :key="column.name"
21
- class="bg-form-view-bg dark:bg-gray-800 border-b dark:border-gray-700"
22
+ v-if="currentValues !== null"
23
+ class="bg-ligftForm dark:bg-gray-800 border-b dark:border-gray-700 block md:table-row"
22
24
  >
23
- <!-- if column is in customComponentsPerColumn, use this component. If not, use this code -->
24
- <template v-if="customComponentsPerColumn[column.name]">
25
- <component
26
- :is="customComponentsPerColumn[column.name]"
27
- :column="column"
28
- :value="currentValues[column.name]"
29
- @update:value="setCurrentValue(column.name, $event)"
30
- />
31
- </template>
32
- <template v-else>
33
- <td class="px-6 py-4 whitespace-nowrap "> <!--align-top-->
25
+ <td class="px-6 py-4 sm:pb-0 whitespace-nowrap flex items-center block md:table-cell"> <!--align-top-->
34
26
  {{ column.label }}
35
- <span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
27
+ <span :data-tooltip-target="`tooltip-show-${i}`" class="ml-1 relative inline-block">
36
28
  <IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
37
29
  :class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
38
30
  />
39
31
  </span>
40
32
  <div :id="`tooltip-show-${i}`"
41
- role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
33
+ role="tooltip"
34
+ class="ml-1 absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
42
35
  Required field
43
36
  <div class="tooltip-arrow" data-popper-arrow></div>
44
37
  </div>
45
38
  </td>
46
- <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
47
- <Dropdown
48
- single
49
- v-if="column.foreignResource"
50
- :options="columnOptions[column.name] || []"
51
- :placeholder = "columnOptions[column.name]?.length || 'There are no options available'"
52
- :modelValue="currentValues[column.name]"
53
- @update:modelValue="setCurrentValue(column.name, $event)"
54
- ></Dropdown>
55
- <Dropdown
56
- single
57
- v-else-if="column.enum"
58
- :options="column.enum"
59
- :modelValue="currentValues[column.name]"
60
- @update:modelValue="setCurrentValue(column.name, $event)"
61
- />
62
- <Dropdown
63
- single
64
- v-else-if="column.type === 'boolean'"
65
- :options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
66
- :modelValue="currentValues[column.name]"
67
- @update:modelValue="setCurrentValue(column.name, $event)"
68
- />
69
- <input
70
- v-else-if="['integer'].includes(column.type)"
71
- type="number"
72
- step="1"
73
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
74
- placeholder="0"
75
- :value="currentValues[column.name]"
76
- @input="setCurrentValue(column.name, $event.target.value)"
77
- >
78
- <CustomDatePicker
79
- v-else-if="['datetime'].includes(column.type)"
39
+ <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative block md:table-cell">
40
+ <template v-if="column?.components?.[props.source]?.file">
41
+ <component
42
+ :is="getCustomComponent(column.components[props.source])"
80
43
  :column="column"
81
- :valueStart="currentValues[column.name]"
82
- auto-hide
83
- @update:valueStart="setCurrentValue(column.name, $event)"
84
- />
85
- <input
86
- v-else-if="['decimal', 'float'].includes(column.type)"
87
- type="number"
88
- step="0.1"
89
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
90
- placeholder="0.0"
91
- :value="currentValues[column.name]"
92
- @input="setCurrentValue(column.name, $event.target.value)"
93
- />
94
- <textarea
95
- v-else-if="['text'].includes(column.type)"
96
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
97
- placeholder="Text"
98
- :value="currentValues[column.name]"
99
- @input="setCurrentValue(column.name, $event.target.value)"
100
- >
101
- </textarea>
102
- <input
103
- v-else
104
- :type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
105
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
106
- placeholder="Text"
107
44
  :value="currentValues[column.name]"
108
- @input="setCurrentValue(column.name, $event.target.value)"
109
- autocomplete="false"
110
- data-lpignore="true"
111
- readonly
112
- onfocus="this.removeAttribute('readonly');"
113
- >
45
+ @update:value="setCurrentValue(column.name, $event)"
46
+ :meta="column.components[props.source].meta"
47
+ :record="currentValues"
48
+ @update:inValidity="customComponentsInValidity[column.name] = $event"
49
+ @update:emptiness="customComponentsEmptiness[column.name] = $event"
50
+ />
51
+ </template>
52
+ <template v-else>
53
+ <Dropdown
54
+ single
55
+ v-if="column.foreignResource"
56
+ :options="columnOptions[column.name] || []"
57
+ :placeholder = "columnOptions[column.name]?.length ?'Select...': 'There are no options available'"
58
+ :modelValue="currentValues[column.name]"
59
+ @update:modelValue="setCurrentValue(column.name, $event)"
60
+ ></Dropdown>
61
+ <Dropdown
62
+ single
63
+ v-else-if="column.enum"
64
+ :options="column.enum"
65
+ :modelValue="currentValues[column.name]"
66
+ @update:modelValue="setCurrentValue(column.name, $event)"
67
+ />
68
+ <Dropdown
69
+ single
70
+ v-else-if="column.type === 'boolean'"
71
+ :options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
72
+ :modelValue="currentValues[column.name]"
73
+ @update:modelValue="setCurrentValue(column.name, $event)"
74
+ />
75
+ <input
76
+ v-else-if="['integer'].includes(column.type)"
77
+ type="number"
78
+ step="1"
79
+ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
80
+ placeholder="0"
81
+ :value="currentValues[column.name]"
82
+ @input="setCurrentValue(column.name, $event.target.value)"
83
+ >
84
+ <CustomDatePicker
85
+ v-else-if="['datetime'].includes(column.type)"
86
+ :column="column"
87
+ :valueStart="currentValues[column.name]"
88
+ auto-hide
89
+ @update:valueStart="setCurrentValue(column.name, $event)"
90
+ />
91
+ <input
92
+ v-else-if="['decimal', 'float'].includes(column.type)"
93
+ type="number"
94
+ step="0.1"
95
+ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
96
+ placeholder="0.0"
97
+ :value="currentValues[column.name]"
98
+ @input="setCurrentValue(column.name, $event.target.value)"
99
+ />
100
+ <textarea
101
+ v-else-if="['text', 'richtext'].includes(column.type)"
102
+ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
103
+ placeholder="Text"
104
+ :value="currentValues[column.name]"
105
+ @input="setCurrentValue(column.name, $event.target.value)"
106
+ >
107
+ </textarea>
108
+ <input
109
+ v-else
110
+ :type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
111
+ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
112
+ placeholder="Text"
113
+ :value="currentValues[column.name]"
114
+ @input="setCurrentValue(column.name, $event.target.value)"
115
+ autocomplete="false"
116
+ data-lpignore="true"
117
+ readonly
118
+ onfocus="this.removeAttribute('readonly');"
119
+ >
114
120
 
115
- <button
116
- v-if="column.masked"
117
- type="button"
118
- @click="unmasked[column.name] = !unmasked[column.name]"
119
- class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
120
- >
121
- <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
122
- <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
123
- </button>
121
+ <button
122
+ v-if="column.masked"
123
+ type="button"
124
+ @click="unmasked[column.name] = !unmasked[column.name]"
125
+ class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
126
+ >
127
+ <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
128
+ <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
129
+ </button>
130
+ </template>
124
131
  <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
125
132
 
126
133
  <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
127
- </td>
128
- </template>
134
+
135
+ </td>
129
136
  </tr>
130
137
 
131
138
  </tbody>
@@ -141,7 +148,7 @@
141
148
  import CustomDatePicker from "@/components/CustomDatePicker.vue";
142
149
  import Dropdown from '@/components/Dropdown.vue';
143
150
  import { useCoreStore } from '@/stores/core';
144
- import { callAdminForthApi } from '@/utils';
151
+ import { callAdminForthApi, getCustomComponent } from '@/utils';
145
152
  import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
146
153
  import { computedAsync } from '@vueuse/core';
147
154
  import { initFlowbite } from 'flowbite';
@@ -155,22 +162,38 @@ const props = defineProps({
155
162
  resource: Object,
156
163
  record: Object,
157
164
  validating: Boolean,
158
- customComponentsPerColumn: Object,
165
+ source: String,
159
166
  });
160
167
 
161
-
162
168
  const unmasked = ref({});
163
169
 
164
170
  const mode = computed(() => props.record && Object.keys(props.record).length ? 'edit' : 'create');
165
171
 
166
172
  const emit = defineEmits(['update:record', 'update:isValid']);
167
173
 
168
- const currentValues = ref({});
174
+ const currentValues = ref(null);
175
+
176
+ const customComponentsInValidity = ref({});
177
+ const customComponentsEmptiness = ref({});
169
178
 
170
179
 
171
180
  const columnError = (column) => {
172
181
  const val = computed(() => {
173
- if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
182
+ if (!currentValues.value) {
183
+ return null;
184
+ }
185
+ if (customComponentsInValidity.value[column.name]) {
186
+ return customComponentsInValidity.value[column.name];
187
+ }
188
+
189
+ if (
190
+ column.required[mode.value] &&
191
+ (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') &&
192
+ // if component is custum it might tell other criteria for emptiness by emitting 'update:emptiness'
193
+ // components which do not emit 'update:emptiness' will have undefined value in customComponentsEmptiness
194
+ (customComponentsEmptiness.value[column.name] !== false)
195
+
196
+ ) {
174
197
  return 'This field is required';
175
198
  }
176
199
  if ( column.type === 'string' || column.type === 'text' ) {
@@ -182,14 +205,17 @@ const columnError = (column) => {
182
205
  }
183
206
  }
184
207
  if ( ['integer', 'decimal', 'float'].includes(column.type) ) {
185
- if ( column.minValue !== undefined && currentValues.value[column.name] < column.minValue ) {
208
+ if ( column.minValue !== undefined
209
+ && currentValues.value[column.name] !== null
210
+ && currentValues.value[column.name] < column.minValue
211
+ ) {
186
212
  return `This field must be greater than ${column.minValue}`;
187
213
  }
188
214
  if ( column.maxValue !== undefined && currentValues.value[column.name] > column.maxValue ) {
189
215
  return `This field must be less than ${column.maxValue}`;
190
216
  }
191
217
  }
192
- if ( column.validation && column.validation.length ){
218
+ if ( column.validation && column.validation.length ) {
193
219
  const validationArray = column.validation;
194
220
  for (let i = 0; i < validationArray.length; i++) {
195
221
  if (validationArray[i].regExp) {
@@ -212,18 +238,22 @@ const columnError = (column) => {
212
238
 
213
239
 
214
240
  const setCurrentValue = (key, value) => {
215
- currentValues.value[key] = value;
216
-
241
+ const col = props.resource.columns.find((column) => column.name === key);
242
+ if (['integer', 'float'].includes(col.type) && (value || value === 0)) {
243
+ currentValues.value[key] = +value;
244
+ } else {
245
+ currentValues.value[key] = value;
246
+ }
247
+ currentValues.value = { ...currentValues.value };
248
+ console.log('3️⃣ setCurrentValue', key, value);
217
249
  emit('update:record', currentValues.value);
218
250
  };
219
251
 
220
252
  onMounted(() => {
221
- Object.keys(props.record).forEach((key) => {
222
- currentValues.value[key] = props.record[key];
223
- });
253
+ currentValues.value = Object.assign({}, props.record);
254
+ console.log('2️⃣ currentValues', JSON.stringify(currentValues.value));
224
255
  initFlowbite();
225
-
226
-
256
+ emit('update:isValid', isValid.value);
227
257
  });
228
258
 
229
259
  const columnOptions = computedAsync(async () => {
@@ -247,7 +277,6 @@ const columnOptions = computedAsync(async () => {
247
277
 
248
278
  }, {});
249
279
 
250
- const coreStore = useCoreStore();
251
280
 
252
281
  const editableColumns = computed(() => {
253
282
  const mode = props.record ? 'edit' : 'create';