Skip to content

Commit

Permalink
now can upload multiple bundles, added bundle validation, improved ve…
Browse files Browse the repository at this point in the history
…rsioning and publishing flow
  • Loading branch information
roncodes committed Mar 25, 2024
1 parent da0ff28 commit 8bb5ee6
Show file tree
Hide file tree
Showing 60 changed files with 1,224 additions and 91 deletions.
1 change: 1 addition & 0 deletions addon/adapters/registry-extension-bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './registry-bridge';
65 changes: 8 additions & 57 deletions addon/components/extension-form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -42,37 +42,15 @@
</TagInput>
</InputGroup>
</ContentPanel>
<ContentPanel @title={{t "registry-bridge.developers.extensions.extension-form.package-name"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name={{t "registry-bridge.developers.extensions.extension-form.package-json-name"}} @value={{@extension.package_name}} />
<InputGroup @name={{t "registry-bridge.developers.extensions.extension-form.composer-json-name"}} @value={{@extension.composer_name}} />
</ContentPanel>
<ContentPanel @title={{t "registry-bridge.developers.extensions.extension-form.extension-bundle"}} @open={{true}} @pad={{false}} @panelBodyClass="bg-white dark:bg-gray-800">
<div class="flex flex-row items-center px-4 pb-4 pt-2">
<FileUpload @name={{t "registry-bridge.developers.extensions.extension-form.extension-upload-bundle"}} @accept={{join "," this.acceptedBundleTypes}} @onFileAdded={{perform this.uploadBundle}} as |queue|>
<a tabindex={{0}} class="flex items-center px-0 mt-2 text-xs no-underline truncate btn btn-sm btn-default" disabled={{not queue.files.length}}>
{{#if queue.files.length}}
<div class="mr-1.5">
<Spinner />
</div>
<span>
{{t "common.uploading"}}
</span>
{{else}}
<FaIcon @icon="box-archive" class="mr-1.5" />
<span>
{{t "registry-bridge.developers.extensions.extension-form.extension-upload-bundle"}}
</span>
{{/if}}
</a>
</FileUpload>
<div>
{{#if @extension.latest_bundle_uuid}}
<div class="flex flex-row items-center mt-2 ml-2 bg-green-800 rounded-lg px-4 py-1 shadow-sm border-green-400">
<FaIcon @icon="box-archive" @size="sm" class="text-green-200 mr-2" />
<span class="text-green-200 text-sm font-mono">{{n-a @extension.latest_bundle_filename}}</span>
</div>
{{/if}}
</div>
<div class="px-4 pb-4 pt-3 flex flex-col flex-grow-0">
<Button @type="magic" @icon="box-archive" @text="Select Bundle" @onClick={{this.selectBundle}} />
{{#if @extension.next_bundle_filename}}
<div class="mt-4 rounded-lg shadow-sm bg-gray-900 border border-gray-200 px-4 py-2 flex flex-row items-center text-xs text-gray-100">
<FaIcon @icon="box-archive" @size="xs" />
<span class="ml-2">{{@extension.next_bundle_filename}}</span>
</div>
{{/if}}
</div>
</ContentPanel>
<ContentPanel @title={{t "registry-bridge.developers.extensions.extension-form.extension-listing-details"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
Expand All @@ -89,33 +67,6 @@
<InputGroup @type="url" @name={{t "registry-bridge.developers.extensions.extension-form.extension-tos-url"}} @value={{@extension.tos_url}} @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-tos-url-help-text"}} @wrapperClass="mb-0i" />
</div>
</ContentPanel>
<ContentPanel @title={{t "registry-bridge.developers.extensions.extension-form.extension-payment-details"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @wrapperClass={{unless @extension.payment_required "mb-0i"}}>
<Toggle @isToggled={{@extension.payment_required}} @onToggle={{fn (mut @extension.payment_required)}} @label={{t "registry-bridge.developers.extensions.extension-form.extension-payment-required"}} @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-payment-required-help-text"}} />
</InputGroup>
{{#if @extension.payment_required}}
<InputGroup>
<Toggle @isToggled={{@extension.subscription_required}} @onToggle={{fn (mut @extension.subscription_required)}} @label={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-required"}} @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-required-help-text"}} />
</InputGroup>
{{#if @extension.subscription_required}}
<InputGroup @name={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-billing-period"}} @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-billing-period-help-text"}}>
<Select @value={{@extension.subscription_billing_period}} @options={{this.billingPeriodOptions}} @onSelect={{fn (mut @extension.subscription_billing_period)}} @placeholder={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-billing-period-placeholder"}} class="w-full" />
</InputGroup>
<InputGroup @name={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-amount"}} @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-amount-help-text"}} @wrapperClass="mb-0i">
<MoneyInput @value={{@extension.subscription_amount}} @currency="USD" />
</InputGroup>
{{else}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
<InputGroup @name={{t "registry-bridge.developers.extensions.extension-form.extension-price"}} @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-price-help-text"}}>
<MoneyInput @value={{@extension.price}} @currency="USD" />
</InputGroup>
<InputGroup @name={{t "registry-bridge.developers.extensions.extension-form.extension-sale-price"}} @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-sale-price-help-text"}}>
<MoneyInput @value={{@extension.sale_price}} @currency="USD" />
</InputGroup>
</div>
{{/if}}
{{/if}}
</ContentPanel>
<ContentPanel @title={{t "registry-bridge.developers.extensions.extension-form.extension-screenshots"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<div class="space-y-4">
{{#if this.isUploading}}
Expand Down
30 changes: 28 additions & 2 deletions addon/components/extension-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default class ExtensionFormComponent extends Component {
@service fetch;
@service notifications;
@service intl;
@service modalsManager;
@tracked subscriptionModelOptions = ['flat_rate', 'tiered', 'usage'];
@tracked billingPeriodOptions = ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'];
@tracked uploadQueue = [];
Expand Down Expand Up @@ -98,8 +99,8 @@ export default class ExtensionFormComponent extends Component {
this.fetch.uploadFile.perform(
file,
{
path: `uploads/extensions/${this.args.extension.id}/screenshots`,
subject_uuid: this.args.extension.id,
path: `uploads/extensions/${extension.id}/screenshots`,
subject_uuid: extension.id,
subject_type: 'registry-bridge:registry-extension',
type: 'extension_screenshot',
},
Expand All @@ -120,6 +121,31 @@ export default class ExtensionFormComponent extends Component {
);
}

@action selectBundle() {
const { extension, onBundleSelected } = this.args;

this.modalsManager.show('modals/select-extension-bundle', {
title: 'Select extension bundle',
modalClass: 'modal-md',
acceptButtonText: 'Done',
hideDeclineButton: true,
extension,
onBundleSelected: (bundle) => {
extension.setProperties({
next_bundle_uuid: bundle.id,
next_bundle_filename: bundle.bundle_filename,
next_bundle: bundle,
});

if (typeof onBundleSelected === 'function') {
onBundleSelected(bundle);
}

this.modalsManager.done();
},
});
}

@action removeFile(file) {
return file.destroyRecord();
}
Expand Down
36 changes: 36 additions & 0 deletions addon/components/modals/select-extension-bundle.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
{{#if this.bundles}}
<div class="next-table-wrapper auto-height">
<table>
<thead>
<tr>
<th {{set-width "175px"}}>ID</th>
<th {{set-width "70px"}}>Bundle</th>
<th {{set-width "80px"}}>Version</th>
<th {{set-width "110px"}}>Uploaded</th>
<th {{set-width "100px"}}>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each this.bundles as |bundle|}}
<tr>
<td>{{bundle.bundle_id}}</td>
<td>{{bundle.bundle_number}}</td>
<td>{{bundle.version}}</td>
<td>{{bundle.createdAgo}}</td>
<td>
<Badge @status={{bundle.status}} />
</td>
<td>
<Button @text="Select" @size="xs" @type="primary" @onClick={{fn this.selectBundle bundle}} />
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{else}}
<div>No bundles uploaded.</div>
{{/if}}
</Modal::Default>
31 changes: 31 additions & 0 deletions addon/components/modals/select-extension-bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';

export default class ModalsSelectExtensionBundleComponent extends Component {
@service store;
@tracked extension;
@tracked options = {};
@tracked bundles = [];

constructor(owner, { options }) {
super(...arguments);
const { extension } = options;

this.options = options;
this.extension = extension;
this.loadExtensionBundles.perform(extension);
}

@task *loadExtensionBundles(extension) {
this.bundles = yield this.store.query('registry-extension-bundle', { extension_uuid: extension.id, status: 'pending' });
}

@action selectBundle(bundle) {
if (typeof this.options.onBundleSelected === 'function') {
this.options.onBundleSelected(bundle);
}
}
}
70 changes: 70 additions & 0 deletions addon/controllers/developers/extensions/edit/bundles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';

export default class DevelopersExtensionsEditBundlesController extends Controller {
@service store;
@service fetch;
@service hostRouter;
@service notifications;
@tracked extension;
@tracked lastError;
acceptedBundleTypes = [
'application/zip',
'application/x-zip',
'application/x-zip-compressed',
'application/x-compressed',
'multipart/x-zip',
'application/x-tar',
'application/gzip',
'application/x-gzip',
'application/x-tgz',
'application/x-bzip2',
'application/x-xz',
];

@task *uploadBundle(file) {
this.lastError = undefined;

yield this.fetch.uploadFile.perform(
file,
{
path: `uploads/extensions/${this.extension.id}/bundles`,
subject_uuid: this.extension.id,
subject_type: 'registry-bridge:registry-extension',
type: 'extension_bundle',
meta: {
version: this.extension.version,
},
},
(uploadedFile) => {
return this.createBundle.perform(uploadedFile);
},
() => {
// remove file from queue
if (file.queue && typeof file.queue.remove === 'function') {
file.queue.remove(file);
}
}
);
}

@task *createBundle(uploadedFile) {
const bundle = this.store.createRecord('registry-extension-bundle', {
extension_uuid: this.extension.id,
bundle_uuid: uploadedFile.id,
bundle: uploadedFile,
status: 'pending',
});

try {
yield bundle.save();
} catch (error) {
this.lastError = error.message;
return this.notifications.serverError(error);
}

yield this.hostRouter.refresh();
}
}
3 changes: 3 additions & 0 deletions addon/controllers/developers/extensions/edit/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Controller from '@ember/controller';

export default class DevelopersExtensionsEditIndexController extends Controller {}
7 changes: 7 additions & 0 deletions addon/controllers/developers/extensions/edit/monetize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';

export default class DevelopersExtensionsEditMonetizeController extends Controller {
@tracked subscriptionModelOptions = ['flat_rate', 'tiered', 'usage'];
@tracked billingPeriodOptions = ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'];
}
62 changes: 62 additions & 0 deletions addon/models/registry-extension-bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Model, { attr, belongsTo } from '@ember-data/model';
import { computed } from '@ember/object';
import { format as formatDate, formatDistanceToNow, isValid as isValidDate } from 'date-fns';

export default class RegistryExtensionBundleModel extends Model {
/** @ids */
@attr('string') uuid;
@attr('string') company_uuid;
@attr('string') created_by_uuid;
@attr('string') extension_uuid;
@attr('string') bundle_uuid;
@attr('string') bundle_id;
@attr('string') public_id;

/** @relationships */
@belongsTo('file') bundle;

/** @attributes */
@attr('string') bundle_filename;
@attr('string') bundle_number;
@attr('string') version;
@attr('string') status;
@attr('object') meta;

/** @dates */
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;

/** @computed */
@computed('updated_at') get updatedAgo() {
if (!isValidDate(this.updated_at)) {
return null;
}

return formatDistanceToNow(this.updated_at);
}

@computed('updated_at') get updatedAt() {
if (!isValidDate(this.updated_at)) {
return null;
}

return formatDate(this.updated_at, 'PP HH:mm');
}

@computed('created_at') get createdAgo() {
if (!isValidDate(this.created_at)) {
return null;
}

return formatDistanceToNow(this.created_at);
}

@computed('created_at') get createdAt() {
if (!isValidDate(this.created_at)) {
return null;
}

return formatDate(this.created_at, 'PP HH:mm');
}
}
12 changes: 8 additions & 4 deletions addon/models/registry-extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ export default class RegistryExtensionModel extends Model {
@attr('string') company_uuid;
@attr('string') created_by_uuid;
@attr('string') registry_user_uuid;
@attr('string') latest_bundle_uuid;
@attr('string') current_bundle_uuid;
@attr('string') next_bundle_uuid;
@attr('string') category_uuid;
@attr('string') icon_uuid;
@attr('string') public_id;
@attr('string') current_bundle_id;

/** @relationships */
@belongsTo('company') company;
@belongsTo('user') user;
@belongsTo('user') created_by;
@belongsTo('category') category;
@belongsTo('file') latest_bundle;
@belongsTo('registry-extension-bundle') current_bundle;
@belongsTo('registry-extension-bundle') next_bundle;
@belongsTo('file') icon;
@hasMany('file') screenshots;

Expand All @@ -41,7 +44,8 @@ export default class RegistryExtensionModel extends Model {
@attr('string', { defaultValue: '1.0.0' }) version;
@attr('string') fa_icon;
@attr('string') description;
@attr('string') latest_bundle_filename;
@attr('string') current_bundle_filename;
@attr('string') next_bundle_filename;
@attr('string') promotional_text;
@attr('string') website_url;
@attr('string') repo_url;
Expand Down
8 changes: 6 additions & 2 deletions addon/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ export default buildRoutes(function () {
this.route('extensions', function () {
this.route('index', { path: '/' });
this.route('new');
this.route('edit', { path: '/edit/:public_id' });
this.route('details', { path: '/:public_id' });
this.route('edit', { path: '/distribution/:public_id' }, function () {
this.route('index', { path: '/' });
this.route('details');
this.route('bundles');
this.route('monetize');
});
});
this.route('analytics');
this.route('payments');
Expand Down
Loading

0 comments on commit 8bb5ee6

Please sign in to comment.