DEV: Migrate publish page modal to Glimmer and DModal (#22663)

This PR migrates the publish page modal to a Glimmer component and DModal.

Most of the code is lift-and-shift. However, the component state getters were implemented using meta-programming in the original controller. They have all been inlined here for clarity, searchability, etc.
This commit is contained in:
Ted Johansson 2023-07-19 10:37:07 +08:00 committed by GitHub
parent 20ec7ac174
commit c2e90f8c07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 272 additions and 232 deletions

View File

@ -0,0 +1,105 @@
<DModal
@closeModal={{@closeModal}}
@title={{i18n "topic.publish_page.title"}}
class="publish-page-modal"
>
<:body>
{{#if this.unpublished}}
<p>{{i18n "topic.publish_page.unpublished"}}</p>
{{else}}
<ConditionalLoadingSpinner @condition={{this.initializing}}>
<p class="publish-description">{{i18n
"topic.publish_page.description"
}}</p>
<form>
<div class="controls">
<label>{{i18n "topic.publish_page.slug"}}</label>
<TextField
@value={{this.publishedPage.slug}}
@onChange={{this.checkSlug}}
@onChangeImmediate={{this.startCheckSlug}}
@disabled={{this.existing}}
@class="publish-slug"
/>
</div>
<div class="controls">
<label>{{i18n "topic.publish_page.public"}}</label>
<p class="description">
<Input
@type="checkbox"
@checked={{readonly this.publishedPage.public}}
{{on "click" this.onChangePublic}}
/>
{{i18n "topic.publish_page.public_description"}}
</p>
</div>
</form>
<div class="publish-url">
<ConditionalLoadingSpinner @condition={{this.checking}} />
{{#if this.existing}}
<div class="current-url">
{{i18n "topic.publish_page.publish_url"}}
<div>
<a
href={{this.publishedPage.url}}
target="_blank"
rel="noopener noreferrer"
>{{this.publishedPage.url}}</a>
</div>
</div>
{{else}}
{{#if this.showUrl}}
<div class="valid-slug">
{{i18n "topic.publish_page.preview_url"}}
<div class="example-url">{{this.publishedPage.url}}</div>
</div>
{{/if}}
{{#if this.invalid}}
{{i18n "topic.publish_page.invalid_slug"}}
<span class="invalid-slug">{{this.reason}}.</span>
{{/if}}
{{/if}}
</div>
</ConditionalLoadingSpinner>
{{/if}}
</:body>
<:footer>
{{#if this.showUnpublish}}
<DButton
@label="topic.publish_page.unpublish"
@icon="trash-alt"
@class="btn-danger"
@isLoading={{this.unpublishing}}
@action={{this.unpublish}}
/>
<DButton
@class="close-publish-page"
@icon="times"
@label="close"
@action={{@closeModal}}
/>
{{else if this.unpublished}}
<DButton
@label="topic.publish_page.publishing_settings"
@action={{this.startNew}}
/>
{{else}}
<DButton
@label="topic.publish_page.publish"
@class="btn-primary publish-page"
@icon="file"
@disabled={{this.disabled}}
@isLoading={{this.saving}}
@action={{this.publish}}
/>
{{/if}}
</:footer>
</DModal>

View File

@ -0,0 +1,165 @@
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
import Component from "@glimmer/component";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
const States = {
initializing: "initializing",
checking: "checking",
valid: "valid",
invalid: "invalid",
saving: "saving",
new: "new",
existing: "existing",
unpublishing: "unpublishing",
unpublished: "unpublished",
};
export default class PublishPageModal extends Component {
@service store;
@tracked state = States.initializing;
@tracked reason = null;
@tracked publishedPage = null;
constructor() {
super(...arguments);
this.store
.find("published_page", this.args.model.id)
.then((page) => {
this.state = States.existing;
this.publishedPage = page;
})
.catch(this.startNew);
}
get initializing() {
return this.state === States.initializing;
}
get checking() {
return this.state === States.checking;
}
get valid() {
return this.state === States.valid;
}
get invalid() {
return this.state === States.invalid;
}
get saving() {
return this.state === States.saving;
}
get new() {
return this.state === States.new;
}
get existing() {
return this.state === States.existing;
}
get unpublishing() {
return this.state === States.unpublishing;
}
get unpublished() {
return this.state === States.unpublished;
}
get disabled() {
return this.state !== States.valid;
}
get showUrl() {
return (
this.state === States.valid ||
this.state === States.saving ||
this.state === States.existing
);
}
get showUnpublish() {
return this.state === States.existing || this.state === States.unpublishing;
}
@action
startCheckSlug() {
if (this.state === States.existing) {
return;
}
this.state = States.checking;
}
@action
checkSlug() {
if (this.state === States.existing) {
return;
}
return ajax("/pub/check-slug", {
data: { slug: this.publishedPage.slug },
}).then((result) => {
if (result.valid_slug) {
this.state = States.valid;
} else {
this.state = States.invalid;
this.reason = result.reason;
}
});
}
@action
unpublish() {
this.state = States.unpublishing;
return this.publishedPage
.destroyRecord()
.then(() => {
this.state = States.unpublished;
this.args.model.set("publishedPage", null);
})
.catch((result) => {
this.state = States.existing;
popupAjaxError(result);
});
}
@action
publish() {
this.state = States.saving;
return this.publishedPage
.update(this.publishedPage.getProperties("slug", "public"))
.then(() => {
this.state = States.existing;
this.args.model.set("publishedPage", this.publishedPage);
})
.catch((errResult) => {
popupAjaxError(errResult);
this.state = States.existing;
});
}
@action
startNew() {
this.state = States.new;
this.publishedPage = this.store.createRecord(
"published_page",
this.args.model.getProperties("id", "slug", "public")
);
this.checkSlug();
}
@action
onChangePublic(event) {
this.publishedPage.set("public", event.target.checked);
if (this.showUnpublish) {
this.publish();
}
}
}

View File

@ -1,130 +0,0 @@
import { action, computed } from "@ember/object";
import { equal, not } from "@ember/object/computed";
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
const States = {
initializing: "initializing",
checking: "checking",
valid: "valid",
invalid: "invalid",
saving: "saving",
new: "new",
existing: "existing",
unpublishing: "unpublishing",
unpublished: "unpublished",
};
const StateHelpers = {};
Object.keys(States).forEach((name) => {
StateHelpers[name] = equal("state", name);
});
export default Controller.extend(ModalFunctionality, StateHelpers, {
state: null,
reason: null,
publishedPage: null,
disabled: not("valid"),
showUrl: computed("state", function () {
return (
this.state === States.valid ||
this.state === States.saving ||
this.state === States.existing
);
}),
showUnpublish: computed("state", function () {
return this.state === States.existing || this.state === States.unpublishing;
}),
onShow() {
this.set("state", States.initializing);
this.store
.find("published_page", this.model.id)
.then((page) => {
this.setProperties({ state: States.existing, publishedPage: page });
})
.catch(this.startNew);
},
@action
startCheckSlug() {
if (this.state === States.existing) {
return;
}
this.set("state", States.checking);
},
@action
checkSlug() {
if (this.state === States.existing) {
return;
}
return ajax("/pub/check-slug", {
data: { slug: this.publishedPage.slug },
}).then((result) => {
if (result.valid_slug) {
this.set("state", States.valid);
} else {
this.setProperties({ state: States.invalid, reason: result.reason });
}
});
},
@action
unpublish() {
this.set("state", States.unpublishing);
return this.publishedPage
.destroyRecord()
.then(() => {
this.set("state", States.unpublished);
this.model.set("publishedPage", null);
})
.catch((result) => {
this.set("state", States.existing);
popupAjaxError(result);
});
},
@action
publish() {
this.set("state", States.saving);
return this.publishedPage
.update(this.publishedPage.getProperties("slug", "public"))
.then(() => {
this.set("state", States.existing);
this.model.set("publishedPage", this.publishedPage);
})
.catch((errResult) => {
popupAjaxError(errResult);
this.set("state", States.existing);
});
},
@action
startNew() {
this.setProperties({
state: States.new,
publishedPage: this.store.createRecord(
"published_page",
this.model.getProperties("id", "slug", "public")
),
});
this.checkSlug();
},
@action
onChangePublic(isPublic) {
this.publishedPage.set("public", isPublic);
if (this.showUnpublish) {
this.publish();
}
},
});

View File

@ -11,6 +11,7 @@ import showModal from "discourse/lib/show-modal";
import TopicFlag from "discourse/lib/flag-targets/topic-flag";
import PostFlag from "discourse/lib/flag-targets/post-flag";
import HistoryModal from "discourse/components/modal/history";
import PublishPageModal from "discourse/components/modal/publish-page";
const SCROLL_DELAY = 500;
@ -112,9 +113,8 @@ const TopicRoute = DiscourseRoute.extend({
@action
showPagePublish() {
const model = this.modelFor("topic");
showModal("publish-page", {
this.modal.show(PublishPageModal, {
model,
title: "topic.publish_page.title",
});
},

View File

@ -1,100 +0,0 @@
<DModalBody>
{{#if this.unpublished}}
<p>{{i18n "topic.publish_page.unpublished"}}</p>
{{else}}
<ConditionalLoadingSpinner @condition={{this.initializing}}>
<p class="publish-description">{{i18n
"topic.publish_page.description"
}}</p>
<form>
<div class="controls">
<label>{{i18n "topic.publish_page.slug"}}</label>
<TextField
@value={{this.publishedPage.slug}}
@onChange={{action "checkSlug"}}
@onChangeImmediate={{action "startCheckSlug"}}
@disabled={{this.existing}}
@class="publish-slug"
/>
</div>
<div class="controls">
<label>{{i18n "topic.publish_page.public"}}</label>
<p class="description">
<Input
@type="checkbox"
@checked={{readonly this.publishedPage.public}}
{{on "click" (action "onChangePublic" value="target.checked")}}
/>
{{i18n "topic.publish_page.public_description"}}
</p>
</div>
</form>
<div class="publish-url">
<ConditionalLoadingSpinner @condition={{this.checking}} />
{{#if this.existing}}
<div class="current-url">
{{i18n "topic.publish_page.publish_url"}}
<div>
<a
href={{this.publishedPage.url}}
target="_blank"
rel="noopener noreferrer"
>{{this.publishedPage.url}}</a>
</div>
</div>
{{else}}
{{#if this.showUrl}}
<div class="valid-slug">
{{i18n "topic.publish_page.preview_url"}}
<div class="example-url">{{this.publishedPage.url}}</div>
</div>
{{/if}}
{{#if this.invalid}}
{{i18n "topic.publish_page.invalid_slug"}}
<span class="invalid-slug">{{this.reason}}.</span>
{{/if}}
{{/if}}
</div>
</ConditionalLoadingSpinner>
{{/if}}
</DModalBody>
<div class="modal-footer">
{{#if this.showUnpublish}}
<DButton
@label="topic.publish_page.unpublish"
@icon="trash-alt"
@class="btn-danger"
@isLoading={{this.unpublishing}}
@action={{action "unpublish"}}
/>
<DButton
@class="close-publish-page"
@icon="times"
@label="close"
@action={{action "closeModal"}}
/>
{{else if this.unpublished}}
<DButton
@label="topic.publish_page.publishing_settings"
@action={{action "startNew"}}
/>
{{else}}
<DButton
@label="topic.publish_page.publish"
@class="btn-primary publish-page"
@icon="file"
@disabled={{this.disabled}}
@isLoading={{this.saving}}
@action={{action "publish"}}
/>
{{/if}}
</div>