Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4.x] Add ability to customize bard/replicator set icons directory #8931

Merged
merged 41 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fc9649e
Show default icon directory and folder settings usin placeholders.
jesseleite Nov 1, 2023
15cce24
Wip.
jesseleite Nov 3, 2023
bca6a1c
Use provider implementation instead.
jesseleite Nov 3, 2023
559a31d
Use user’s config in set / set group icon selectors.
jesseleite Nov 3, 2023
6aa229a
Allow use of named arguments, if they just want to pass `$folder`.
jesseleite Nov 3, 2023
ca573de
Rework `<svg-icon>` tp support custom icons.
jesseleite Nov 3, 2023
5c62922
Revert "Rework `<svg-icon>` tp support custom icons."
jesseleite Nov 3, 2023
b77c210
Flesh out custom icons static method, on `Sets` instead of `Bard`.
jesseleite Nov 3, 2023
11c68d2
Use svg html passed from server in set picker.
jesseleite Nov 3, 2023
2402423
Reference more generic keys in blueprint icon selectors.
jesseleite Nov 3, 2023
1f7485c
Backend handles this now.
jesseleite Nov 3, 2023
bd0f4ea
Rename setter method.
jesseleite Nov 3, 2023
7be4eab
No comment
jesseleite Nov 3, 2023
05329cd
Cleanup fallback handling.
jesseleite Nov 6, 2023
0e0027c
Camel case like other provided json vars.
jesseleite Nov 6, 2023
828d65e
Ensure we provide default icon directory and folder to script.
jesseleite Nov 6, 2023
54dcf66
Get icon html here as well.
jesseleite Nov 6, 2023
e4f9c3c
Render using v-html.
jesseleite Nov 6, 2023
eba892b
Set default icon.
jesseleite Nov 6, 2023
c00b8a6
Docblock.
jesseleite Nov 6, 2023
2fe35bc
Fix issue with custom `$folder` not being respected.
jesseleite Nov 11, 2023
b22d254
Prepare custom SVG icon contents for script.
jesseleite Nov 11, 2023
c9dd8d6
Provide custom SVG icon contents to script.
jesseleite Nov 11, 2023
83ada6d
Revert this icon html functionality, since we’re providing that to sc…
jesseleite Nov 11, 2023
d4a28c1
We’re in a Vue component, so just use methods instead of vanilla JS f…
jesseleite Nov 11, 2023
6a09a1c
Render using `<svg-icon>` again, but pass custom directory.
jesseleite Nov 11, 2023
6c10fb2
If custom icon is available, render that instead.
jesseleite Nov 11, 2023
8f71579
Clean up naming.
jesseleite Nov 11, 2023
4ee45da
Update set picker as well.
jesseleite Nov 11, 2023
e9ba7b3
Fix fallback.
jesseleite Nov 11, 2023
fc98238
Always show icon components.
jesseleite Nov 11, 2023
f3af7cb
Show custom icon, or fallback to Statamic when not available.
jesseleite Nov 11, 2023
bd795c5
Pass failing tests.
jesseleite Nov 11, 2023
cda8fe1
Test that user can provide custom icon base dir and/or sub-folder to …
jesseleite Nov 11, 2023
b8cb0f5
We shouldn’t need to provide default directory to script.
jesseleite Nov 13, 2023
99a847c
Mounted is cleaner than created + nextTick.
jesseleite Nov 13, 2023
f81bd8e
Fix rendering of icons in blueprint builder.
jesseleite Nov 13, 2023
d5e7254
Fix fallback icon handling.
jesseleite Nov 13, 2023
b1c74c4
Fix more fallback icon handling.
jesseleite Nov 13, 2023
b44b6ba
Fix custom icon handling when configuring custom directory AND sub-fo…
jesseleite Nov 13, 2023
698b0c5
Update tests.
jesseleite Nov 13, 2023
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
57 changes: 42 additions & 15 deletions resources/js/components/SvgIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,76 @@

<script>
import { defineAsyncComponent } from 'vue';

const splitIcon = function(icon) {
if (! icon.includes('/')) icon = 'regular/' + icon;
return icon.split('/');
}

const fallbackIconImport = function() {
return import('./../../svg/icons/regular/image.svg');
}
import { data_get } from '../bootstrap/globals.js'

export default {

props: {
name: String,
default: String,
directory: String,
},

data() {
return {
icon: this.evaluateIcon(),
icon: null,
}
},

created() {
this.$nextTick(() => this.icon = this.evaluateIcon());
},

watch: {
name() {
this.icon = this.evaluateIcon();
}
},

computed: {
customIcon() {
if (! this.directory) return;

return data_get(this.$config.get('customSvgIcons') || {}, `${this.directory}.${this.name}`);
},
},

methods: {
evaluateIcon() {
if (this.customIcon) {
return defineAsyncComponent(() => {
return new Promise(resolve => resolve({ template: this.customIcon }));
});
}

if (this.name.startsWith('<svg')) {
return defineAsyncComponent(() => {
return new Promise(resolve => resolve({ template: this.name }));
});
}

return defineAsyncComponent(() => {
const [set, file] = splitIcon(this.name);
const [set, file] = this.splitIcon(this.name);

return import(`./../../svg/icons/${set}/${file}.svg`)
.catch(e => {
if (! this.default) return fallbackIconImport();
const [set, file] = splitIcon(this.default);
return import(`./../../svg/icons/${set}/${file}.svg`).catch(e => fallbackIconImport());
if (! this.default) return this.fallbackIconImport();
const [set, file] = this.splitIcon(this.default);
return import(`./../../svg/icons/${set}/${file}.svg`).catch(e => this.fallbackIconImport());
});
});
}
},

splitIcon(icon) {
if (! icon.includes('/')) icon = 'regular/' + icon;

return icon.split('/');
},

fallbackIconImport() {
return import('./../../svg/icons/regular/image.svg');
},

}
}
</script>
26 changes: 23 additions & 3 deletions resources/js/components/blueprints/Section.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<div class="blueprint-drag-handle blueprint-section-drag-handle w-4 border-r"></div>
<div class="p-2 flex-1 flex items-center">
<a class="flex items-center flex-1 group" @click="edit">
<svg-icon class="h-4 w-4 mr-2 text-gray-700 group-hover:text-blue-500" :name="section.icon ? `plump/${section.icon}` : 'folder-generic'" />
<svg-icon :name="section.icon" :directory="iconDirectory" class="h-4 w-4 mr-2 text-gray-700 group-hover:text-blue-500" />
<div class="mr-2" v-text="section.display" />
</a>
<button class="flex items-center text-gray-700 hover:text-gray-950 mr-3" @click="edit">
Expand Down Expand Up @@ -42,7 +42,7 @@
<div class="form-group w-full" v-if="showHandleField">
<label v-text="__('Icon')" />
<publish-field-meta
:config="{ handle: 'icon', type: 'icon', folder: 'plump' }"
:config="{ handle: 'icon', type: 'icon', directory: this.iconBaseDirectory, folder: this.iconSubFolder }"
:initial-value="editingSection.icon"
v-slot="{ meta, value, loading }"
>
Expand Down Expand Up @@ -122,9 +122,29 @@ export default {
},

computed: {

suggestableConditionFields() {
return this.suggestableConditionFieldsProvider?.suggestableFields || [];
}
},

iconBaseDirectory() {
return this.$config.get('setIconsDirectory');
},

iconSubFolder() {
return this.$config.get('setIconsFolder');
},

iconDirectory() {
let dir = this.iconBaseDirectory;

if (this.iconSubFolder) {
dir = dir+'/'+this.iconSubFolder;
}

return dir;
},

},

watch: {
Expand Down
22 changes: 20 additions & 2 deletions resources/js/components/blueprints/Tab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@click="$emit('selected')"
@mouseenter="$emit('mouseenter')"
>
<svg-icon v-if="tab.icon" :name="`plump/${tab.icon}`" class="w-4 h-4 mr-1" />
<svg-icon :name="tab.icon" :directory="iconDirectory" class="w-4 h-4 mr-1" />

{{ tab.display }}

Expand Down Expand Up @@ -58,7 +58,7 @@
<div class="form-group w-full" v-if="showInstructions">
<label v-text="__('Icon')" />
<publish-field-meta
:config="{ handle: 'icon', type: 'icon', folder: 'plump' }"
:config="{ handle: 'icon', type: 'icon', directory: this.iconBaseDirectory, folder: this.iconSubFolder }"
:initial-value="icon"
v-slot="{ meta, value, loading }"
>
Expand Down Expand Up @@ -117,6 +117,24 @@ export default {
return this.currentTab === this.tab._id;
},

iconBaseDirectory() {
return this.$config.get('setIconsDirectory');
},

iconSubFolder() {
return this.$config.get('setIconsFolder');
},

iconDirectory() {
let dir = this.iconBaseDirectory;

if (this.iconSubFolder) {
dir = dir+'/'+this.iconSubFolder;
}

return dir;
},

},

methods: {
Expand Down
21 changes: 14 additions & 7 deletions resources/js/components/fieldtypes/replicator/SetPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,15 @@
<div class="p-1 max-h-[21rem] overflow-auto">
<div v-for="(item, i) in items" :key="item.handle" class="cursor-pointer rounded" :class="{ 'bg-gray-200': selectionIndex === i }" @mouseover="selectionIndex = i">
<div v-if="item.type === 'group'" @click="selectGroup(item.handle)" class="flex items-center group px-2 py-1.5 rounded-md">
<div class="h-9 w-9 rounded bg-white border border-gray-600 mr-2 p-2">
<svg-icon :name="item.icon ? `plump/${item.icon}` : 'folder-generic'" class="text-gray-800" />
</div>
<svg-icon :name="item.icon" :directory="iconDirectory" class="h-9 w-9 rounded bg-white border border-gray-600 mr-2 p-2 text-gray-800" />
<div class="flex-1">
<div class="text-md font-medium text-gray-800 truncate w-52">{{ __(item.display || item.handle) }}</div>
<div v-if="item.instructions" class="text-2xs text-gray-700 truncate w-52">{{ __(item.instructions) }}</div>
</div>
<svg-icon name="micro/chevron-right-thin" class="text-gray-600 group-hover:text-gray-800" />
</div>
<div v-if="item.type === 'set'" @click="addSet(item.handle)" class="flex items-center group px-2 py-1.5 rounded-md">
<div class="h-9 w-9 rounded bg-white border border-gray-600 mr-2 p-2">
<svg-icon :name="item.icon ? `plump/${item.icon}` : 'light/add'" class="text-gray-800" />
</div>
<svg-icon :name="item.icon" :directory="iconDirectory" class="h-9 w-9 rounded bg-white border border-gray-600 mr-2 p-2 text-gray-800" />
<div class="flex-1">
<div class="text-md font-medium text-gray-800 truncate w-52">{{ __(item.display || item.handle) }}</div>
<div v-if="item.instructions" class="text-2xs text-gray-700 truncate w-52">{{ __(item.instructions) }}</div>
Expand Down Expand Up @@ -139,7 +135,18 @@ export default {

noSearchResults() {
return this.search && this.visibleSets.length === 0;
}
},

iconDirectory() {
let iconDirectory = this.$config.get('setIconsDirectory');
let iconFolder = this.$config.get('setIconsFolder');

if (iconFolder) {
iconDirectory = iconDirectory+'/'+iconFolder;
}

return iconDirectory;
},

},

Expand Down
34 changes: 34 additions & 0 deletions src/Fieldtypes/Icon.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace Statamic\Fieldtypes;

use Illuminate\Filesystem\Filesystem;
use Statamic\Facades\File;
use Statamic\Facades\Folder;
use Statamic\Facades\Path;
use Statamic\Fields\Fieldtype;
use Statamic\Support\Str;

class Icon extends Fieldtype
{
Expand All @@ -14,6 +16,8 @@ class Icon extends Fieldtype
protected $categories = ['media'];
protected $icon = 'icon_picker';

protected static $customSvgIcons = [];

public function preload(): array
{
[$path, $directory, $folder, $hasConfiguredDirectory] = $this->resolveParts();
Expand All @@ -39,11 +43,13 @@ protected function configFieldItems(): array
'display' => __('Directory'),
'instructions' => __('statamic::fieldtypes.icon.config.directory'),
'type' => 'text',
'placeholder' => 'vendor/statamic/cms/resources/svg/icons',
],
'folder' => [
'display' => __('Folder'),
'instructions' => __('statamic::fieldtypes.icon.config.folder'),
'type' => 'text',
'placeholder' => static::DEFAULT_FOLDER,
],
'default' => [
'display' => __('Default Value'),
Expand Down Expand Up @@ -85,4 +91,32 @@ private function resolveParts()
$hasConfiguredDirectory,
];
}

/**
* Provide custom SVG icons to script.
*
* @param string $directory
* @param string|null $folder
*/
public static function provideCustomSvgIconsToScript($directory, $folder = null)
{
$path = Str::removeRight(Path::tidy($directory.'/'.$folder), '/');

static::$customSvgIcons[$path] = collect(app(Filesystem::class)->files($path))
->filter(fn ($file) => strtolower($file->getExtension()) === 'svg')
->keyBy(fn ($file) => pathinfo($file->getBasename(), PATHINFO_FILENAME))
->map
->getContents()
->all();
}

/**
* Get custom SVG icons for script.
*
* @return array
*/
public static function getCustomSvgIcons()
{
return static::$customSvgIcons;
}
}
59 changes: 50 additions & 9 deletions src/Fieldtypes/Sets.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@

namespace Statamic\Fieldtypes;

use Statamic\Facades\File;
use Statamic\Fields\Fieldset;
use Statamic\Fields\FieldTransformer;
use Statamic\Fields\Fieldtype;
use Statamic\Statamic;
use Statamic\Support\Arr;

class Sets extends Fieldtype
{
protected $selectable = false;

protected static $iconsDirectory = 'vendor/statamic/cms/resources/svg/icons';
protected static $iconsFolder = 'plump';

/**
* Converts the "sets" array of a Replicator (or Bard) field into what the
* <sets-fieldtype> Vue component is expecting, within either the Blueprint
Expand Down Expand Up @@ -41,14 +46,14 @@ public function preProcess($sets)
'handle' => $groupHandle,
'display' => $group['display'] ?? null,
'instructions' => $group['instructions'] ?? null,
'icon' => $group['icon'] ?? null,
'icon' => $group['icon'] ?? 'regular/folder-generic',
'sections' => collect($group['sets'] ?? [])->map(function ($set, $setHandle) use ($groupId) {
return [
'_id' => $setId = $groupId.'-section-'.$setHandle,
'handle' => $setHandle,
'display' => $set['display'] ?? null,
'instructions' => $set['instructions'] ?? null,
'icon' => $set['icon'] ?? null,
'icon' => $set['icon'] ?? 'regular/folder-generic',
'fields' => collect($set['fields'])->map(function ($field, $i) use ($setId) {
return array_merge(FieldTransformer::toVue($field), ['_id' => $setId.'-'.$i]);
})->all(),
Expand Down Expand Up @@ -83,13 +88,16 @@ public function preProcessConfig($sets)
return collect($sets)->map(function ($group, $groupHandle) {
return array_merge($group, [
'handle' => $groupHandle,
'sets' => collect($group['sets'])->map(function ($config, $name) {
return array_merge($config, [
'handle' => $name,
'id' => $name,
'fields' => (new NestedFields)->preProcessConfig(array_get($config, 'fields', [])),
]);
})
'icon' => $group['icon'] ?? 'regular/folder-generic',
'sets' => collect($group['sets'])
->map(function ($config, $name) {
return array_merge($config, [
'handle' => $name,
'id' => $name,
'fields' => (new NestedFields)->preProcessConfig(array_get($config, 'fields', [])),
'icon' => $config['icon'] ?? 'light/add',
]);
})
->values()
->all(),
]);
Expand Down Expand Up @@ -126,4 +134,37 @@ public function process($tabs)
})
->all();
}

/**
* Allow the user to set custom icon directory and/or folder for SVG set icons.
*
* @param string|null $directory
* @param string|null $folder
*/
public static function setIconsDirectory($directory = null, $folder = null)
{
// If they are specifying new base directory, ensure we do not assume sub-folder
if ($directory) {
static::$iconsDirectory = $directory;
static::$iconsFolder = $folder;
}

// Of if they are specifying just a sub-folder, use that with original base directory
elseif ($folder) {
static::$iconsFolder = $folder;
}

// Then provide to script for <icon-fieldtype> selector components in blueprint config
Statamic::provideToScript([
'setIconsDirectory' => static::$iconsDirectory,
'setIconsFolder' => static::$iconsFolder,
]);

// And finally, provide the file contents of all custom svg icons to script,
// but only if custom directory because our <svg-icon> component cannot
// reference custom paths at runtime without a full Vite re-build
if ($directory) {
Icon::provideCustomSvgIconsToScript($directory, $folder);
}
}
}
Loading
Loading