Skip to content
Draft
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
4 changes: 4 additions & 0 deletions resources/js/bootstrap/cp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import AssetIndexes from '@/components/utilities/AssetIndexes/AssetIndexes.vue';
import SystemMessages from '@/components/utilities/SystemMessages/SystemMessages.vue';
import DeprecationErrorsToolbar from '@/components/utilities/DeprecationErrors/DeprecationErrorsToolbar.vue';
import {setTranslations} from '@craftcms/cp/utilities/translate.ts.mjs';
import LocalFsSettings from '@/components/Filesystems/LocalFsSettings.vue';

let bootedCallbacks: Array<(instance: any) => void> = [];
let bootingCallbacks: Array<(instance: any) => void> = [];
Expand Down Expand Up @@ -98,6 +99,9 @@ const Cp = {
app.component('ProjectConfig', ProjectConfig);
app.component('AssetIndexes', AssetIndexes);
app.component('SystemMessages', SystemMessages);
app.component('LocalFsSettings', LocalFsSettings);

// @TODO Register plugin components
},
});

Expand Down
73 changes: 73 additions & 0 deletions resources/js/components/Filesystems/FilesystemSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup lang="ts">
import {t} from '@craftcms/cp';
import {computed} from 'vue';
import {usePage} from '@inertiajs/vue3';
import type {Filesystem} from '@/pages/SettingsFilesystemsEditPage.vue';
import VarDump from '@/components/VarDump.vue';
import CraftSwitch from '@craftcms/cp/vue/CraftSwitch.vue';
import CraftCombobox from '@/components/form/CraftCombobox.vue';
import DynamicHtmlRenderer from '@/components/DynamicHtmlRenderer.vue';

const hasUrls = defineModel<boolean>('hasUrls');
const url = defineModel<string>('url');
withDefaults(
defineProps<{
filesystem?: Filesystem;
}>(),
{
filesystem: {
errors: () => [],
name: null,
handle: null,
oldHandle: null,
hasUrls: false,
url: null,
uid: null,
rootUrl: null,
id: null,
dateCreated: null,
dateUpdated: null,
settingsHtml: null,
rootPath: null,
path: null,
},
}
);

const page = usePage<{
readOnly: boolean;
baseUrlSuggestions?: Array<any>;
}>();
</script>

<template>
<div v-if="filesystem" :id="filesystem.type">
<template v-if="filesystem.showHasUrlSetting">
<CraftSwitch
:label="t('Files in this filesystem have public URLs')"
name="hasUrls"
id="has-urls"
v-model="hasUrls"
:disabled="page.props.readOnly"
/>

<template v-if="hasUrls && filesystem.showUrlSetting">
<CraftCombobox
:label="t('Base URL')"
:help-text="t('The base URL to the files in this filesystem.')"
v-model="url"
:options="page.props.baseUrlSuggestions"
name="url"
:required="true"
placeholder="//example.com/path/to/folder"
data-error-key="url"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we always have to provide this or could we fall back to what's provided in name for example and only need to provide this when it's different than the input name?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we'll want something like that. I just haven't quite worked out exactly where we'll want the logic. data-error-key will likely be obsolete relatively soon (as far as I know it's only to link the error summary to the field itself).

We could create a useCraftField that would return all of these attributes. Ideally I'd like a place to centralize this logic so we don't have to worry (as much) about keeping all our fields up to date with things like this, and so we can make sure they all follow the same logic for setting those.

My other thought is to eventually have some kind of <CraftForm/> component that would look over the fields within it and makes sure they have everything we want.

Since I haven't quite figured that out, I've just been adding things manually for the moment.

:disabled="page.props.readOnly"
></CraftCombobox>
</template>
</template>
<DynamicHtmlRenderer :html="filesystem.settingsHtml"/>
<!--<div v-html="filesystem.settingsHtml" />-->
</div>
</template>

<style scoped lang="scss"></style>
18 changes: 18 additions & 0 deletions resources/js/components/Filesystems/LocalFsSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
import {computed} from 'vue';
import VarDump from '@/components/VarDump.vue';

const props = defineProps<{
filesystem?: string;
}>();

const fs = computed(() => props.filesystem ? JSON.parse(props.filesystem) : null);
</script>

<template>
<h2>Roundabout LocalFsSettings</h2>

<VarDump :data="{props}" />
</template>

<style scoped lang="scss"></style>
130 changes: 130 additions & 0 deletions resources/js/pages/SettingsFilesystemsEditPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<script setup lang="ts">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make sense to start putting pages in folders, for example pages/settings/filesystems/EditFilesystemPage.vue

Seb wrote a blog on how Spatie structures Inertia apps and there might be some ideas in there we could use as well.

For example if there are components needed just for filesystems we could put them in pages/settings/filesystems/components

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #18964 to restructure things based on their opinions. I'll hopefully pull that in soon so we don't have too many PRs to rebase

import {t, toHandle} from '@craftcms/cp';
import AppLayout from '@/layout/AppLayout.vue';
import VarDump from '@/components/VarDump.vue';
import type {SelectItem, SelectOption} from '@/types';
import Pane from '@/components/Pane.vue';
import CraftInput from '@craftcms/cp/vue/CraftInput.vue';
import CraftInputHandle from '@craftcms/cp/vue/CraftInputHandle.vue';
import Select from '@/components/form/Select.vue';
import {useForm} from '@inertiajs/vue3';
import {useInputGenerator} from '@/composables/useInputGenerator';
import DynamicHtmlRenderer from '@/components/DynamicHtmlRenderer.vue';
import FilesystemSettings from '@/components/Filesystems/FilesystemSettings.vue';

export type Filesystem = {
errors: any[];
name: string | null;
handle: string | null;
oldHandle: any;
hasUrls: boolean;
url: any;
uid: any;
rootUrl: any;
id: any;
dateCreated: any;
dateUpdated: any;
settingsHtml: string;
rootPath: string;
path: any;
type: string;
showHasUrlSetting: boolean;
showUrlSetting: boolean;
};

const props = defineProps<{
oldHandle: string | null;
filesystem: Filesystem;
fsOptions: Array<SelectOption>;
fsInstances: Record<string, Filesystem>;
fsTypes: Array<string>;
readOnly: boolean;
}>();

const form = useForm({
name: props.filesystem.name ?? '',
handle: props.filesystem.handle ?? '',
type: props.filesystem.type ?? '',
hasUrls: props.filesystem.hasUrls ?? false,
url: props.filesystem.url ?? '',
});

useInputGenerator(
() => form.name,
(v) => (form.handle = toHandle(v))
);

function handleSubmit() {
const form = event.target as HTMLFormElement;

form.transform(() => {
return {
...(new FormData(form)),
...form
}
}).submit();
}

function getFs(fsType: string): Filesystem | null {
if (fsType === form.type) {
return props.filesystem;
}

return props.fsInstances[fsType] ?? null;
}
</script>

<template>
<AppLayout>
<Pane appearance="raised">
<div class="grid gap-3">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably good to have a <FormFields> wrapper that defines the spacing, just so it's consistent everywhere and if it has to change it's only in 1 file, or have <Pane> be a grid by default

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm planning to add a <craft-field-group/> component that will handle the spacing as well as the layout system for fields.

<CraftInput
v-model="form.name"
:label="t('Name')"
id="name"
name="name"
:autofocus="true"
:required="true"
:error="form.errors?.name"
data-error-key="name"
:disabled="readOnly"
/>

<CraftInputHandle
:label="t('Handle')"
id="handle"
name="handle"
v-model="form.handle"
:required="true"
:error="form.errors?.handle"
data-error-key="handle"
:disabled="readOnly"
/>

<hr />

<template v-if="fsOptions.length">
<Select
id="type"
name="type"
:label="t('Filesystem Type')"
:help-text="t('What type of filesystem is this?')"
:options="fsOptions"
v-model="form.type"
:disabled="readOnly"
/>
</template>

<template v-for="fsType in fsTypes">
<FilesystemSettings
v-model:has-urls="form.hasUrls"
v-model:url="form.url"
:filesystem="fsInstances[fsType]"
/>
</template>
</div>
</Pane>
</AppLayout>
</template>

<style scoped lang="scss"></style>
32 changes: 19 additions & 13 deletions resources/templates/_components/fs/Local/settings.twig
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
{% from "_includes/forms" import autosuggestField %}

<craft-input
label="{{ "Base Path"|t('app') }}"
:help-text="{{ "The base folder path that should be used as the root of the filesystem."|t('app') }}"
name="path"
></craft-input>

{{ autosuggestField({
label: "Base Path"|t('app'),
instructions: "The base folder path that should be used as the root of the filesystem."|t('app'),
id: 'path',
class: 'ltr',
name: 'path',
suggestEnvVars: true,
suggestAliases: true,
value: filesystem.path,
required: true,
placeholder: "/path/to/folder"|t('app'),
errors: filesystem.errors.get('path'),
data: {'error-key': 'path'},
disabled: readOnly,
label: "Base Path"|t('app'),
instructions: "The base folder path that should be used as the root of the filesystem."|t('app'),
id: 'path',
class: 'ltr',
name: 'path',
suggestEnvVars: true,
suggestAliases: true,
value: filesystem.path,
required: true,
placeholder: "/path/to/folder"|t('app'),
errors: filesystem.errors.get('path'),
data: {'error-key': 'path'},
disabled: readOnly,
}) }}
14 changes: 11 additions & 3 deletions src/Filesystem/Filesystems/Local.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use CraftCms\Cms\Support\Env;
use CraftCms\Cms\Support\Facades\Security;
use CraftCms\Cms\Support\File;
use CraftCms\Cms\Support\Html;
use CraftCms\Cms\View\TemplateMode;
use Override;

Expand Down Expand Up @@ -124,10 +125,17 @@ public function getReadOnlySettingsHtml(): ?string

private function settingsHtml(bool $readOnly): string
{
return template('_components/fs/Local/settings', [
'filesystem' => $this,
return Html::tag('LocalFsSettings', attributes: [
// ':filesystem' => json_encode([
// 'name' => $this->name,
// 'handle' => $this->handle,
// ]),
'readOnly' => $readOnly,
], TemplateMode::Cp);
]);
// return template('_components/fs/Local/settings', [
// 'filesystem' => $this,
// 'readOnly' => $readOnly,
// ], TemplateMode::Cp);
}

#[Override]
Expand Down
26 changes: 23 additions & 3 deletions src/Http/Controllers/Settings/FilesystemsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@

use CraftCms\Cms\Config\GeneralConfig;
use CraftCms\Cms\Cp\Html\ContentHtml;
use CraftCms\Cms\Cp\SelectOptions;
use CraftCms\Cms\Filesystem\Contracts\FsInterface;
use CraftCms\Cms\Filesystem\Filesystems;
use CraftCms\Cms\Filesystem\Resources\FsResource;
use CraftCms\Cms\Http\RespondsWithFlash;
use CraftCms\Cms\Http\Responses\CpScreenResponse;
use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\Support\Html;
use CraftCms\Cms\Support\Str;
use CraftCms\Cms\Support\Url;
use Illuminate\Http\Request;
use Inertia\Inertia;
Expand Down Expand Up @@ -93,18 +95,36 @@
$title = t('Create a new filesystem');
}

$isValidUrl = fn ($value) => Str::isUrl($value);

return new CpScreenResponse()
->title($title)
->addCrumb(t('Settings'), 'settings')
->addCrumb(t('Filesystems'), 'settings/filesystems')
->contentTemplate('settings/filesystems/_edit.twig', [
->inertiaPage('SettingsFilesystemsEditPage', [
'oldHandle' => $handle,
'filesystem' => $filesystem,
'filesystem' => [
...$filesystem,

Check failure on line 107 in src/Http/Controllers/Settings/FilesystemsController.php

View workflow job for this annotation

GitHub Actions / Code Quality / Phpstan

Only iterables can be unpacked, CraftCms\Cms\Filesystem\Contracts\FsInterface given.
'type' => $filesystem::class,
'settingsHtml' => $this->readOnly ? $filesystem->getReadOnlySettingsHtml() : $filesystem->getSettingsHtml(),
'showHasUrlSetting' => $filesystem->getShowHasUrlSetting(),
'showUrlSetting' => $filesystem->getShowUrlSetting(),
],
'fsOptions' => $fsOptions,
'fsInstances' => $fsInstances,
'fsTypes' => $allFsTypes,
'readOnly' => $this->readOnly,

// @TODO this should probably be its own item on SelectOptions
'baseUrlSuggestions' => SelectOptions::getEnvSuggestions(true, $isValidUrl),
])
// ->contentTemplate('settings/filesystems/_edit.twig', [
// 'oldHandle' => $handle,
// 'filesystem' => $filesystem,
// 'fsOptions' => $fsOptions,
// 'fsInstances' => $fsInstances,
// 'fsTypes' => $allFsTypes,
// 'readOnly' => $this->readOnly,
// ])
->unless(
$this->readOnly,
function (CpScreenResponse $response) {
Expand Down
Loading