Skip to content
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
2 changes: 1 addition & 1 deletion resources/js/form/components/Field.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Editor from "./fields/editor/Editor.vue";
import Geolocation from "./fields/geolocation/Geolocation.vue";
import Html from "./fields/Html.vue";
import List from "./fields/List.vue";
import List from "./fields/list/List.vue";
import Number from "./fields/Number.vue";
import Select from "./fields/select/Select.vue";
import Tags from "./fields/Tags.vue";
Expand Down
9 changes: 3 additions & 6 deletions resources/js/form/components/Form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
import debounce from "lodash/debounce";
import { api } from "@/api/api";
import { route } from "@/utils/url";
import { useParentCommands } from "@/commands/useCommands";
import merge from 'lodash/merge';
import { useFieldContainerData } from "@/form/useFieldContainerData";

const props = defineProps<{
form: Form
Expand Down Expand Up @@ -82,14 +82,11 @@
props.form.setMeta(fieldKey, { uploading });
}

const parentCommands = useParentCommands();
const fieldContainerData = useFieldContainerData(props.form);
const refresh = debounce((data) => {
api.post(route('code16.sharp.api.form.refresh.update', {
entityKey: props.form.entityKey,
instance_id: props.form.instanceId,
embed_key: props.form.embedKey,
entity_list_command_key: parentCommands?.commandContainer === 'entityList' ? props.form.commandKey : null,
show_command_key: parentCommands?.commandContainer === 'show' ? props.form.commandKey : null,
...fieldContainerData,
}), data)
.then(response => {
merge(props.form.data, response.data.form.data);
Expand Down
19 changes: 9 additions & 10 deletions resources/js/form/components/fields/Autocomplete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import {
FormAutocompleteItemData,
FormAutocompleteLocalFieldData,
FormAutocompleteRemoteFieldData,
FormAutocompleteRemoteFieldData
} from "@/types";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { computed, ref } from "vue";
Expand All @@ -21,12 +21,12 @@
import { route } from "@/utils/url";
import { api } from "@/api/api";
import { useParentForm } from "@/form/useParentForm";
import { isCancel } from "axios";
import { ComboboxItemIndicator } from "reka-ui";
import { useParentCommands } from "@/commands/useCommands";
import { useIsInDialog } from "@/components/ui/dialog/Dialog.vue";
import { useFullTextSearch } from "@/composables/useFullTextSearch";
import { useRemoteAutocomplete } from "@/composables/useRemoteAutocomplete";
import { useFieldContainerData } from "@/form/useFieldContainerData";
import { useParentListField } from "@/form/components/fields/list/useParentListField";

const props = defineProps<FormFieldProps<FormAutocompleteLocalFieldData | FormAutocompleteRemoteFieldData>>();
const emit = defineEmits<FormFieldEmits<FormAutocompleteLocalFieldData | FormAutocompleteRemoteFieldData>>();
Expand All @@ -36,7 +36,6 @@
const searchTerm = ref('');
const results = ref<FormAutocompleteItemData[]>([]);

const parentCommands = useParentCommands();
const isInDialog = useIsInDialog();
const { fullTextSearch } = useFullTextSearch(
() => props.field.mode === 'local' ? props.field.localValues : null,
Expand All @@ -45,19 +44,19 @@
searchKeys: props.field.mode === 'local' ? props.field.searchKeys : [],
}
);
const parentListField = useParentListField();
const fieldContainerData = useFieldContainerData(form);
const { loading, search: remoteSearch } = useRemoteAutocomplete(({ query, signal, onSuccess, onError }) => {
const field = props.field as FormAutocompleteRemoteFieldData;
return api.post(
route('code16.sharp.api.form.autocomplete.index', {
entityKey: form.entityKey,
autocompleteFieldKey: props.parentField ? `${props.parentField.key}.${field.key}` : field.key,
embed_key: form.embedKey,
entity_list_command_key: parentCommands?.commandContainer === 'entityList' ? form.commandKey : null,
show_command_key: parentCommands?.commandContainer === 'show' ? form.commandKey : null,
dashboard_command_key: parentCommands?.commandContainer === 'dashboard' ? form.commandKey : null,
instance_id: form.instanceId,
autocompleteFieldKey: parentListField && parentListField.form === form
? `${parentListField.props.field.key}.${field.key}`
: field.key,
endpoint: field.remoteEndpoint,
search: query,
...fieldContainerData,
}), {
formData: field.callbackLinkedFields
? Object.fromEntries(
Expand Down
3 changes: 2 additions & 1 deletion resources/js/form/components/fields/editor/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@
uploadModal,
embedManager,
embedModal,
} satisfies ParentEditor);
form,
});
watch(() => [embedManager.contentEmbeds, uploadManager.contentUploads], () => {
emit('input', {
Expand Down
5 changes: 3 additions & 2 deletions resources/js/form/components/fields/editor/useParentEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ import EditorUploadModal from "@/form/components/fields/editor/extensions/upload
import EditorEmbedModal from "@/form/components/fields/editor/extensions/embed/EditorEmbedModal.vue";

/**
* @see import('./Editor.vue') -> provide('editor')
* @see import('./Editor.vue')
*/
export type ParentEditor = {
props: FormFieldProps<FormEditorFieldData>,
embedManager: ContentEmbedManager<Form>,
embedModal: Ref<InstanceType<typeof EditorEmbedModal>>
uploadManager: ContentUploadManager<Form>,
uploadModal: Ref<InstanceType<typeof EditorUploadModal>>,
form: Form,
};

export function useParentEditor() {
return inject<ParentEditor>('editor');
return inject<ParentEditor>('editor', null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useParentForm } from "@/form/useParentForm";
import { FormFieldData, FormListFieldData, FormUploadFieldData, FormUploadFieldValueData } from "@/types";
import { getDependantFieldsResetData } from "@/form/util";
import { computed, nextTick, ref, watch, watchEffect } from "vue";
import { computed, nextTick, provide, ref, watch, watchEffect } from "vue";
import { Button, buttonVariants } from '@/components/ui/button';
import { showAlert } from "@/utils/dialogs";
import { FieldMeta, FieldsMeta, FormFieldEmitInputOptions, FormFieldEmits, FormFieldProps } from "@/form/types";
Expand All @@ -22,11 +22,18 @@
import { useSortable } from "@vueuse/integrations/useSortable";
import { useEventListener, watchArray } from "@vueuse/core";
import { FormEvents } from "@/form/Form";
import { ParentListField } from "@/form/components/fields/list/useParentListField";

const props = defineProps<FormFieldProps<FormListFieldData>>();
const emit = defineEmits<FormFieldEmits<FormListFieldData>>();

const form = useParentForm();

provide<ParentListField>('listField', {
props,
form,
});

const canAddItem = computed(() => {
const { field, value } = props;
return field.addable &&
Expand Down Expand Up @@ -226,7 +233,6 @@
:field="form.getField(itemFieldLayout.key, field.itemFields, item, props.field.readOnly)"
:field-layout="itemFieldLayout"
:field-error-key="`${field.key}.${item[errorIndex] ?? item[itemKey]}.${itemFieldLayout.key}`"
:parent-field="field"
:value="item[itemFieldLayout.key]"
:locale="(form.getMeta(`${field.key}.${item[itemKey]}.${itemFieldLayout.key}`) as FieldMeta)?.locale ?? form.defaultLocale"
:parent-data="item"
Expand Down
16 changes: 16 additions & 0 deletions resources/js/form/components/fields/list/useParentListField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FormFieldProps } from "@/form/types";
import { FormListFieldData } from "@/types";
import { inject } from "vue";
import { Form } from "@/form/Form";

export type ParentListField = {
props: FormFieldProps<FormListFieldData>,
form: Form,
}

/**
* @see import('./List.vue')
*/
export function useParentListField() {
return inject<ParentListField>('listField', null);
}
29 changes: 23 additions & 6 deletions resources/js/form/components/fields/upload/Upload.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<script setup lang="ts">
import { FormUploadFieldData } from "@/types";
import Uppy, { MinimalRequiredUppyFile } from '@uppy/core';
import Uppy from '@uppy/core';
import type { UppyFile } from "@uppy/core";
import ThumbnailGenerator from '@uppy/thumbnail-generator';
import XHRUpload from '@uppy/xhr-upload';
import DropTarget from '@uppy/drop-target';
import Cropper from 'cropperjs';
Expand Down Expand Up @@ -42,6 +41,9 @@
} from "@/components/ui/dialog";
import { rotate, rotateTo } from "@/form/components/fields/upload/util/rotate";
import { createThumbnail } from "@/form/components/fields/upload/util/thumbnail";
import { useFieldContainerData } from "@/form/useFieldContainerData";
import { useParentEditor } from "@/form/components/fields/editor/useParentEditor";
import { useParentListField } from "@/form/components/fields/list/useParentListField";

const props = defineProps<FormFieldProps<FormUploadFieldData> & {
asEditorEmbed?: boolean,
Expand All @@ -67,6 +69,8 @@
(e: 'edit', event: CustomEvent): void
}>();
const form = useParentForm();
const parentEditor = useParentEditor();
const parentListField = useParentListField();
const transformedImg = ref<string>();
const persistedEditableImg = ref<string>();
const playablePreviewUrl = ref<string>();
Expand All @@ -75,6 +79,22 @@
return props.value && canTransform(props.value.name, props.value.mime_type) && !props.hasError
|| !!props.dropdownEditLabel;
});
const fieldContainerData = useFieldContainerData(form);
const uploadEndpoint = computed(() => {
// we check form equality because the field may be in an editor embed
// so parentEditor can be the embed parent editor and not an embed form field
let uploadFieldKey = parentEditor && parentEditor.form === form
? parentEditor.props.field.key
: props.field.key;
if(parentListField && parentListField.form === form) {
uploadFieldKey = `${parentListField.props.field.key}.${uploadFieldKey}`;
}
return route('code16.sharp.api.form.upload', {
entityKey: form.entityKey,
uploadFieldKey,
...fieldContainerData,
});
});
const uppy = new Uppy({
id: props.fieldErrorKey,
restrictions: {
Expand All @@ -92,12 +112,9 @@
pluralize: () => 1,
},
autoProceed: true,
meta: {
'validation_rule[]': props.field.validationRule,
},
})
.use(XHRUpload, {
endpoint: route('code16.sharp.api.form.upload'),
endpoint: uploadEndpoint.value,
fieldName: 'file',
headers: {
'Accept': 'application/json',
Expand Down
1 change: 0 additions & 1 deletion resources/js/form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export type WithDynamicAttributesApplied<Data extends FormFieldData> =

export type FormFieldProps<Data extends FormFieldData = FormFieldData, Value = Data['value']> = {
field: WithDynamicAttributesApplied<Data>,
parentField?: FormListFieldData
fieldLayout?: LayoutFieldData,
fieldErrorKey?: string,
parentData?: FormFieldData | FormListFieldData['value'][number],
Expand Down
16 changes: 16 additions & 0 deletions resources/js/form/useFieldContainerData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RequestFieldContainerData } from "@/types";
import { useParentCommands } from "@/commands/useCommands";
import { Form } from "@/form/Form";


export function useFieldContainerData(form: Form): RequestFieldContainerData {
const parentCommands = useParentCommands();

return {
embed_key: form.embedKey,
entity_list_command_key: parentCommands?.commandContainer === 'entityList' ? form.commandKey : null,
show_command_key: parentCommands?.commandContainer === 'show' ? form.commandKey : null,
dashboard_command_key: parentCommands?.commandContainer === 'dashboard' ? form.commandKey : null,
instance_id: form.instanceId,
};
}
9 changes: 7 additions & 2 deletions resources/js/types/generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export type BreadcrumbData = {
items: Array<BreadcrumbItemData>;
};
export type BreadcrumbItemData = {
type: string;
label: string;
documentTitleLabel: string | null;
entityKey: string;
Expand Down Expand Up @@ -812,6 +811,13 @@ export type PieGraphWidgetData = {
ratioY: number | null;
height: number | null;
};
export type RequestFieldContainerData = {
embed_key: string | null;
entity_list_command_key: string | null;
show_command_key: string | null;
dashboard_command_key: string | null;
instance_id: string | number | null;
};
export type SearchResultLinkData = {
link: string;
label: string;
Expand Down Expand Up @@ -850,7 +856,6 @@ export type ShowConfigData = {
isSingle: boolean;
commands: ConfigCommandsData | null;
titleAttribute: string | null;
breadcrumbAttribute: string | null;
editButtonLabel: string | null;
state: EntityStateData | null;
};
Expand Down
15 changes: 14 additions & 1 deletion resources/js/types/routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,20 @@ declare module 'ziggy-js' {
"required": false
}
],
"code16.sharp.api.form.upload": [],
"code16.sharp.api.form.upload": [
{
"name": "globalFilter"
},
{
"name": "entityKey",
"required": true,
"binding": "key"
},
{
"name": "uploadFieldKey",
"required": true
}
],
"code16.sharp.api.form.autocomplete.index": [
{
"name": "globalFilter"
Expand Down
28 changes: 28 additions & 0 deletions src/Data/RequestFieldContainerData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Code16\Sharp\Data;

/**
* @internal
*/
final class RequestFieldContainerData extends Data
{
public function __construct(
public ?string $embed_key,
public ?string $entity_list_command_key,
public ?string $show_command_key,
public ?string $dashboard_command_key,
public string|int|null $instance_id,
) {}

public static function from(array $request): self
{
return new self(
embed_key: $request['embed_key'] ?? null,
entity_list_command_key: $request['entity_list_command_key'] ?? null,
show_command_key: $request['show_command_key'] ?? null,
dashboard_command_key: $request['dashboard_command_key'] ?? null,
instance_id: $request['instance_id'] ?? null,
);
}
}
25 changes: 16 additions & 9 deletions src/Http/Controllers/Api/ApiFormUploadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,34 @@

namespace Code16\Sharp\Http\Controllers\Api;

use Code16\Sharp\Exceptions\SharpInvalidConfigException;
use Code16\Sharp\Form\Fields\SharpFormEditorField;
use Code16\Sharp\Utils\Entities\ValueObjects\EntityKey;
use Code16\Sharp\Utils\FileUtil;
use Illuminate\Foundation\Validation\ValidatesRequests;

class ApiFormUploadController extends ApiController
{
use HandlesFieldContainer;
use ValidatesRequests;

public function store(FileUtil $fileUtil)
public function store(string $globalFilter, EntityKey $entityKey, string $uploadFieldKey, FileUtil $fileUtil)
{
$this->validate(request(), [
'validation_rule' => ['nullable', 'array'],
'validation_rule.*' => [
'string',
'regex:/^(file$|image:?|mimes:|mimetypes:|extensions:|dimensions:|size:|between:|min:|max:)/',
],
]);
$field = $this->getFieldContainer($entityKey)
->findFieldByKey($uploadFieldKey);

if ($field instanceof SharpFormEditorField) {
$field = $field->uploadsConfig();
}

if (! $field) {
throw new SharpInvalidConfigException('Upload field '.$uploadFieldKey.' was not found in form.');
}

$this->validate(request(), [
'file' => [
'required',
...request()->input('validation_rule') ?? ['file'],
...$field->toArray()['validationRule'],
],
]);

Expand Down
Loading
Loading