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

[5.x] Improve duplicate asset upload handling #10959

Merged
merged 21 commits into from
Oct 16, 2024
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
13 changes: 12 additions & 1 deletion resources/js/components/assets/Browser/Browser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@
<uploads
v-if="uploads.length"
:uploads="uploads"
class="-mt-px"
:allow-selecting-existing="allowSelectingExistingUpload"
:class="{ '-mt-px': !hasSelections, 'mt-10': hasSelections }"
@existing-selected="existingUploadSelected"
/>

<div class="overflow-x-auto overflow-y-hidden">
Expand Down Expand Up @@ -317,6 +319,7 @@ export default {
initialEditingAssetId: String,
autoselectUploads: Boolean,
autofocusSearch: Boolean,
allowSelectingExistingUpload: Boolean
},

data() {
Expand Down Expand Up @@ -581,6 +584,14 @@ export default {
this.$toast.error(upload.errorMessage);
},

existingUploadSelected(upload) {
const path = `${this.folder.path}/${upload.basename}`.replace(/^\/+/, '');
const id = `${this.container.id}::${path}`;

this.selectedAssets.push(id);
this.$emit('selections-updated', this.selectedAssets);
},

openFileBrowser() {
this.$refs.uploader.browse();
},
Expand Down
4 changes: 3 additions & 1 deletion resources/js/components/assets/Selector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
:query-scopes="queryScopes"
:autoselect-uploads="true"
:autofocus-search="true"
allow-selecting-existing-upload
@selections-updated="selectionsUpdated"
@asset-doubleclicked="select">
@asset-doubleclicked="select"
>

<template slot="contextual-actions" v-if="browserSelections.length">
<button class="btn action mb-6" @click="browserSelections = []">{{ __('Uncheck All') }}</button>
Expand Down
77 changes: 67 additions & 10 deletions resources/js/components/assets/Upload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

<div class="flex items-center my-4" :class="{'text-red-500': status == 'error'}">

<div class="mx-2 flex items-center">
<svg-icon name="micro/warning" class="text-red-500 h-4 w-4" v-if="status === 'error'" />
<loading-graphic v-else :inline="true" text="" />
</div>
<div class="flex items-center flex-1">
<div class="mx-2 flex items-center">
<svg-icon name="micro/warning" class="text-red-500 h-4 w-4" v-if="status === 'error'" />
<loading-graphic v-else :inline="true" text="" />
</div>

<div class="filename">{{ basename }}</div>
<div class="filename">{{ basename }}</div>
</div>

<div
v-if="status !== 'error'"
Expand All @@ -17,13 +19,29 @@
:style="{ width: percent+'%' }" />
</div>

<div class="px-2" v-if="status === 'error'">
<div class="ml-4 px-2 flex items-center gap-2" v-if="status === 'error'">
{{ error }}
<button @click.prevent="clear" class="flex items-center text-gray-700 dark:text-dark-175 hover:text-gray-800 dark:text-dark-100">
<svg-icon name="micro/circle-with-cross" class="h-4 w-4" />
</button>
<dropdown-list v-if="errorStatus === 422">
<template #trigger>
<button class="ml-4 btn btn-xs" v-text="`${__('Fix')}...`" />
</template>
<dropdown-item @click="retryAndOverwrite" :text="__('messages.uploader_overwrite_existing')" />
<dropdown-item @click="openNewFilenameModal" :text="`${__('messages.uploader_choose_new_filename')}...`" />
<dropdown-item @click="retryWithTimestamp" :text="__('messages.uploader_append_timestamp')" />
<dropdown-item @click="selectExisting" v-if="allowSelectingExisting" :text="__('messages.uploader_discard_use_existing')" />
</dropdown-list>
<button class="btn btn-xs" @click="clear" v-text="__('Discard')" />
</div>

<confirmation-modal
v-if="showNewFilenameModal"
:title="__('New Filename')"
@cancel="showNewFilenameModal = false"
@confirm="confirmNewFilename"
>
<text-input autoselect v-model="newFilename" @keydown.enter="confirmNewFilename" />
</confirmation-modal>

</div>

</template>
Expand All @@ -32,8 +50,21 @@
<script>
export default {

props: ['extension', 'basename', 'percent', 'error'],
props: {
extension: String,
basename: String,
percent: Number,
error: String,
errorStatus: Number,
allowSelectingExisting: Boolean,
},

data() {
return {
showNewFilenameModal: false,
newFilename: '',
}
},

computed: {

Expand All @@ -54,6 +85,32 @@ export default {

clear() {
this.$emit('clear');
},

retryAndOverwrite() {
this.$emit('retry', { option: 'overwrite' });
},

retryWithTimestamp() {
this.$emit('retry', { option: 'timestamp' });
},

openNewFilenameModal() {
this.showNewFilenameModal = true;
this.newFilename = this.basename.substring(0, this.basename.lastIndexOf('.'));
},

confirmNewFilename() {
this.showNewFilenameModal = false;
this.retryWithNewFilename();
},

retryWithNewFilename() {
this.$emit('retry', { option: 'rename', filename: this.newFilename})
},

selectExisting() {
this.$emit('existing-selected');
}

}
Expand Down
26 changes: 20 additions & 6 deletions resources/js/components/assets/Uploader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -117,19 +117,21 @@ export default {
}
},

addFile(file) {
addFile(file, data = {}) {
if (! this.enabled) return;

const id = uniqid();
const upload = this.makeUpload(id, file);
const upload = this.makeUpload(id, file, data);

this.uploads.push({
id,
basename: file.name,
extension: file.name.split('.').pop(),
percent: 0,
errorMessage: null,
instance: upload
errorStatus: null,
instance: upload,
retry: (opts) => this.retry(id, opts)
});
},

Expand All @@ -141,10 +143,10 @@ export default {
return this.uploads.findIndex(u => u.id === id);
},

makeUpload(id, file) {
makeUpload(id, file, data = {}) {
const upload = new Upload({
url: this.url,
form: this.makeFormData(file),
form: this.makeFormData(file, data),
headers: {
Accept: 'application/json'
}
Expand All @@ -157,7 +159,7 @@ export default {
return upload;
},

makeFormData(file) {
makeFormData(file, data = {}) {
const form = new FormData();

form.append('file', file);
Expand All @@ -166,6 +168,10 @@ export default {
form.append(key, this.extraData[key]);
}

for (let key in data) {
form.append(key, data[key]);
}

return form;
},

Expand Down Expand Up @@ -212,8 +218,16 @@ export default {
}
}
upload.errorMessage = msg;
upload.errorStatus = status;
this.$emit('error', upload, this.uploads);
this.processUploadQueue();
},

retry(id, args) {
let file = this.findUpload(id).instance.form.get('file');
this.addFile(file, args);
this.uploads.splice(this.findUploadIndex(id), 1);
}
}

}
Expand Down
18 changes: 17 additions & 1 deletion resources/js/components/assets/Uploads.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
:extension="upload.extension"
:percent="upload.percent"
:error="upload.errorMessage"
:error-status="upload.errorStatus"
:allow-selecting-existing="allowSelectingExisting"
@clear="clearUpload(i)"
@retry="retry(i, $event)"
@existing-selected="existingSelected(i)"
/>
</div>

Expand All @@ -20,7 +24,10 @@ import Upload from './Upload.vue';

export default {

props: ['uploads'],
props: {
uploads: Array,
allowSelectingExisting: Boolean,
},


components: {
Expand All @@ -32,6 +39,15 @@ export default {

clearUpload(i) {
this.uploads.splice(i, 1);
},

retry(i, args) {
this.uploads[i].retry(args);
},

existingSelected(i) {
this.$emit('existing-selected', this.uploads[i]);
this.clearUpload(i);
}

}
Expand Down
18 changes: 18 additions & 0 deletions resources/js/components/fieldtypes/assets/AssetsFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
<uploads
v-if="uploads.length"
:uploads="uploads"
allow-selecting-existing
@existing-selected="uploadSelected"
/>

<template v-if="expanded">
Expand Down Expand Up @@ -599,6 +601,22 @@ export default {
const newFolder = response[0].path;
this.update(this.value.map(id => id.replace(`::${this.folder}`, `::${newFolder}`)));
this.lockedDynamicFolder = this.configuredFolder ? newFolder.replace(`${this.configuredFolder}/`, '') : newFolder;
},

uploadSelected(upload) {
const path = `${this.folder}/${upload.basename}`.replace(/^\/+/, '');
const id = `${this.container}::${path}`;

this.uploads.splice(this.uploads.indexOf(upload), 1);

if (this.value.includes(id)) return;

if (this.maxFiles === 1) {
this.loadAssets([id]);
} else {
this.loadAssets([...this.value, id]);
}

}
},

Expand Down
4 changes: 4 additions & 0 deletions resources/lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@
'updater_require_version_command' => 'To require a specific version, run the following command',
'updater_update_to_latest_command' => 'To update to the latest version, run the following command',
'updates_available' => 'Updates are available!',
'uploader_overwrite_existing' => 'Overwrite existing file',
'uploader_choose_new_filename' => 'Choose new filename',
'uploader_append_timestamp' => 'Append timestamp',
'uploader_discard_use_existing' => 'Discard and use existing file',
'user_activation_email_not_sent_error' => 'The activation email couldn\'t be sent. Please check your email configuration and try again.',
'user_groups_handle_instructions' => 'Used to reference this user group on the frontend. It\'s non-trivial to change later.',
'user_groups_intro' => 'User groups allow you to organize users and apply permission-based roles in aggregate.',
Expand Down
4 changes: 4 additions & 0 deletions resources/lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@
'unique_user_value' => 'This value has already been taken.',
'unique_uri' => 'This URI has already been taken.',
'time' => 'Not a valid time.',
'asset_current_filename' => 'This is the current filename.',
'asset_file_exists' => 'A file already exists with this name.',
'asset_file_exists_same_content' => 'A file already exists with this name and has the same content. You may want to delete this rather than rename it.',
'asset_file_exists_different_content' => 'A file already exists with this name but has different content. You may want to replace the other file with this one instead.',

/*
|--------------------------------------------------------------------------
Expand Down
12 changes: 10 additions & 2 deletions src/Actions/RenameAsset.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Statamic\Actions;

use Statamic\Contracts\Assets\Asset;
use Statamic\Rules\AvailableAssetFilename;

class RenameAsset extends Action
{
Expand All @@ -16,6 +17,11 @@ public function visibleTo($item)
return $item instanceof Asset;
}

public function visibleToBulk($items)
{
return false;
}

public function authorize($user, $asset)
{
return $user->can('rename', $asset);
Expand Down Expand Up @@ -44,14 +50,16 @@ public function run($assets, $values)

protected function fieldItems()
{
$asset = $this->items->first();

return [
'filename' => [
'type' => 'text',
'display' => __('Filename'),
'validate' => 'required', // TODO: Better filename validation
'validate' => ['required', new AvailableAssetFilename($asset)],
'classes' => 'mousetrap',
'focus' => true,
'default' => $value = $this->items->containsOneItem() ? $this->items->first()->filename() : null,
'default' => $value = $asset->filename(),
'placeholder' => $value,
'debounce' => false,
'autoselect' => true,
Expand Down
2 changes: 1 addition & 1 deletion src/Assets/AssetUploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ protected function uploadPath(UploadedFile $file)
$ext = strtolower($ext);
}

$filename = self::getSafeFilename(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME));
$filename = self::getSafeFilename($this->asset->filename());

$directory = $this->asset->folder();
$directory = ($directory === '.') ? '/' : $directory;
Expand Down
23 changes: 23 additions & 0 deletions src/Assets/UploadedReplacementFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Statamic\Assets;

use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Http\UploadedFile;

class UploadedReplacementFile extends ReplacementFile
{
public function __construct(private UploadedFile $file)
{
}

public function extension()
{
return $this->file->getClientOriginalExtension();
}

public function writeTo(Filesystem $disk, $path)
{
$disk->putFileAs($this->file, $path);
}
}
Loading
Loading