Browse Source

Merge pull request #16186 from pateljannat/discussions-component-redesign

feat: discussions component redesign
version-14
Suraj Shetty 3 years ago
committed by GitHub
parent
commit
be3d2a9732
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 431 additions and 243 deletions
  1. +9
    -9
      cypress/integration/discussions.js
  2. +14
    -0
      frappe/public/scss/website/index.scss
  3. +1
    -4
      frappe/templates/discussions/button.html
  4. +1
    -1
      frappe/templates/discussions/comment_box.html
  5. +102
    -53
      frappe/templates/discussions/discussions.js
  6. +29
    -17
      frappe/templates/discussions/discussions_section.html
  7. +43
    -7
      frappe/templates/discussions/reply_card.html
  8. +31
    -22
      frappe/templates/discussions/reply_section.html
  9. +2
    -9
      frappe/templates/discussions/search.html
  10. +17
    -12
      frappe/templates/discussions/sidebar.html
  11. +132
    -88
      frappe/templates/styles/discussion_style.css
  12. +17
    -17
      frappe/tests/ui_test_helpers.py
  13. +25
    -3
      frappe/website/doctype/discussion_reply/discussion_reply.py
  14. +8
    -1
      frappe/website/doctype/discussion_topic/discussion_topic.py

+ 9
- 9
cypress/integration/discussions.js View File

@@ -37,24 +37,24 @@ context('Discussions', () => {
};

const reply_through_comment_box = () => {
cy.get('.discussion-on-page:visible .comment-field')
cy.get('.discussion-form:visible .comment-field')
.type('This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.')
.should('have.value', 'This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.');

cy.get('.discussion-on-page:visible .submit-discussion').click();
cy.get('.discussion-form:visible .submit-discussion').click();
cy.wait(3000);
cy.get('.discussion-on-page:visible').should('have.class', 'show');
cy.get('.discussion-on-page:visible').children(".reply-card").eq(1).children(".reply-text")
cy.get('.discussion-on-page:visible').children(".reply-card").eq(1).find(".reply-text")
.should('have.text', 'This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.\n');
};

const cancel_and_clear_comment_box = () => {
cy.get('.discussion-on-page:visible .comment-field')
cy.get('.discussion-form:visible .comment-field')
.type('This is a discussion from the cypress ui tests.')
.should('have.value', 'This is a discussion from the cypress ui tests.');

cy.get('.discussion-on-page:visible .cancel-comment').click();
cy.get('.discussion-on-page:visible .comment-field').should('have.value', '');
cy.get('.discussion-form:visible .cancel-comment').click();
cy.get('.discussion-form:visible .comment-field').should('have.value', '');
};

const single_thread_discussion = () => {
@@ -62,13 +62,13 @@ context('Discussions', () => {
cy.get('.discussions-sidebar').should('have.length', 0);
cy.get('.reply').should('have.length', 0);

cy.get('.discussion-on-page .comment-field')
cy.get('.discussion-form:visible .comment-field')
.type('This comment is being made on a single thread discussion.')
.should('have.value', 'This comment is being made on a single thread discussion.');

cy.get('.discussion-on-page .submit-discussion').click();
cy.get('.discussion-form:visible .submit-discussion').click();
cy.wait(3000);
cy.get('.discussion-on-page').children(".reply-card").eq(-1).children(".reply-text")
cy.get('.discussion-on-page').children(".reply-card").eq(-1).find(".reply-text")
.should('have.text', 'This comment is being made on a single thread discussion.\n');
};



+ 14
- 0
frappe/public/scss/website/index.scss View File

@@ -88,6 +88,20 @@
border-radius: $dropdown-border-radius;
}

.dropdown-item:active {
color: var(--fg-color);
text-decoration: none;
background-color: var(--gray-600);
}

.dropdown-item:active:hover {
color: var(--fg-color);
}

.dropdown-menu a:hover {
cursor: pointer;
}

.input-dark {
background-color: $dark;
border-color: darken($primary, 40%);


+ 1
- 4
frappe/templates/discussions/button.html View File

@@ -1,9 +1,6 @@
{% if frappe.session.user != "Guest" and
(condition is not defined or (condition is defined and condition )) %}
<span class="btn btn-md btn-default reply">
<span class="btn btn-md btn-secondary-dark reply">
{{ _(cta_title) }}
<!-- Below svg is not a part of the current design. Hence it is commented.
The comment will be removed after all design changes are implemented. -->
<!-- <svg class="icon icon-sm ml-1"><use href="#icon-add" style="stroke: var(--gray-700)"></use></svg> -->
</span>
{% endif %}

+ 1
- 1
frappe/templates/discussions/comment_box.html View File

@@ -28,7 +28,7 @@
</div>

<a class="dark-links cancel-comment hide"> {{ _("Cancel") }} </a>
<div class="btn btn-md btn-default submit-discussion pull-right mb-1">
<div class="btn btn-sm btn-default submit-discussion pull-right mb-1">
{{ _("Post") }}
</div>
</div>


+ 102
- 53
frappe/templates/discussions/discussions.js View File

@@ -4,8 +4,6 @@ frappe.ready(() => {

add_color_to_avatars();

expand_first_discussion();

$(".search-field").keyup((e) => {
search_topic(e);
});
@@ -14,11 +12,11 @@ frappe.ready(() => {
show_new_topic_modal(e);
});

$("#login-from-discussion").click((e) => {
$(".login-from-discussion").click((e) => {
login_from_discussion(e);
});

$(".sidebar-topic").click((e) => {
$(".sidebar-parent").click((e) => {
if ($(e.currentTarget).attr("aria-expanded") == "true") {
e.stopPropagation();
}
@@ -31,17 +29,6 @@ frappe.ready(() => {
}
});

$(document).on("input", ".discussion-on-page .comment-field", (e) => {
if ($(e.currentTarget).val()) {
$(e.currentTarget).css("height", "48px");
$(".cancel-comment").removeClass("hide").addClass("show");
$(e.currentTarget).css("height", $(e.currentTarget).prop("scrollHeight"));
} else {
$(".cancel-comment").removeClass("show").addClass("hide");
$(e.currentTarget).css("height", "48px");
}
});

$(document).on("click", ".submit-discussion", (e) => {
submit_discussion(e);
});
@@ -50,16 +37,26 @@ frappe.ready(() => {
clear_comment_box();
});

if ($(document).width() <= 550) {
$(document).on("click", ".sidebar-parent", () => {
hide_sidebar();
});
}
$(document).on("click", ".sidebar-parent", () => {
hide_sidebar();
});

$(document).on("click", ".back", (e) => {
$(document).on("click", ".back-button", (e) => {
back_to_sidebar(e);
});

$(document).on("click", ".dismiss-reply", (e) => {
dismiss_reply(e);
});

$(document).on("click", ".reply-card .dropdown-menu", (e) => {
perform_action(e);
});

$(document).on("input", ".discussion-on-page .comment-field", (e) => {
adjust_comment_box(e);
});

});

const show_new_topic_modal = (e) => {
@@ -79,10 +76,17 @@ const setup_socket_io = () => {
if (window.dev_server) {
frappe.boot.socketio_port = "9000";
}

frappe.socketio.init(9000);
frappe.socketio.socket.on("publish_message", (data) => {
publish_message(data);
});
frappe.socketio.socket.on("update_message", (data) => {
update_message(data);
});
frappe.socketio.socket.on("delete_message", (data) => {
delete_message(data);
});
});
};

@@ -92,44 +96,47 @@ const publish_message = (data) => {
const topic = data.topic_info;
const single_thread = $(".is-single-thread").length;
const first_topic = !$(".reply-card").length;
const document_match_found = doctype == topic.reference_doctype && docname == topic.reference_docname;
const document_match_found = (doctype == topic.reference_doctype) && (docname == topic.reference_docname);

post_message_cleanup();
data.template = hide_actions_on_conditions(data.template, data.reply_owner);
data.template = style_avatar_frame(data.template);
data.sidebar = style_avatar_frame(data.sidebar);
data.new_topic_template = style_avatar_frame(data.new_topic_template);

if ($(`.discussion-on-page[data-topic=${topic.name}]`).length) {
post_message_cleanup();
data.template = style_avatar_frame(data.template);
$('<div class="card-divider-dark mb-8"></div>' + data.template)
.insertBefore(`.discussion-on-page[data-topic=${topic.name}] .discussion-form`);
$(data.template).insertBefore(`.discussion-on-page[data-topic=${topic.name}] .discussion-form`);

} else if (!first_topic && !single_thread && document_match_found) {
post_message_cleanup();
data.new_topic_template = style_avatar_frame(data.new_topic_template);

$(data.sidebar).insertAfter(`.discussions-sidebar .form-group`);
$(data.sidebar).insertBefore($(`.discussions-sidebar .sidebar-parent`).first());
$(`#discussion-group`).prepend(data.new_topic_template);

if (topic.owner == frappe.session.user) {
$(".discussion-on-page") && $(".discussion-on-page").collapse();
$(".sidebar-topic").first().click();
$(".sidebar-parent").first().click();
}

} else if (single_thread && document_match_found) {
post_message_cleanup();
data.template = style_avatar_frame(data.template);
$(data.template).insertBefore(`.discussion-form`);
$(".discussion-on-page").attr("data-topic", topic.name);

} else if (topic.owner == frappe.session.user && document_match_found) {
post_message_cleanup();
window.location.reload();
}

update_reply_count(topic.name);
};

const update_message = (data) => {
const reply_card = $(`[data-reply=${data.reply_name}]`);
reply_card.find(".reply-body").removeClass("hide");
reply_card.find(".reply-edit-card").addClass("hide");
reply_card.find(".reply-text").html(data.reply);
reply_card.find(".reply-actions").addClass("hide");
};

const post_message_cleanup = () => {
$(".topic-title").val("");
$(".comment-field").val("");
$(".discussion-on-page .comment-field").css("height", "48px");
$(".discussion-form .comment-field").val("");
$("#discussion-modal").modal("hide");
$("#no-discussions").addClass("hide");
$(".cancel-comment").addClass("hide");
@@ -141,15 +148,6 @@ const update_reply_count = (topic) => {
$(`[data-target='#t${topic}']`).find(".reply-count").text(reply_count);
};

const expand_first_discussion = () => {
if ($(document).width() > 550) {
$($(".discussions-parent .collapse")[0]).addClass("show");
$($(".discussions-sidebar [data-toggle='collapse']")[0]).attr("aria-expanded", true);
} else {
$("#discussion-group").addClass("hide");
}
};

const search_topic = (e) => {
let input = $(e.currentTarget).val();

@@ -160,7 +158,7 @@ const search_topic = (e) => {
}

topics.each((i, elem) => {
let topic_id = $(elem).parent().attr("data-target");
let topic_id = $(elem).closest(".sidebar-parent").attr("data-target");

/* Check match in replies */
let match_in_reply = false;
@@ -201,16 +199,20 @@ const submit_discussion = (e) => {
e.preventDefault();
e.stopImmediatePropagation();

const target = $(e.currentTarget);
const reply_name = target.closest(".reply-card").data("reply");
const title = $(".topic-title:visible").length ? $(".topic-title:visible").val().trim() : "";
const reply = $(".comment-field:visible").val().trim();
let reply = reply_name ? target.closest(".reply-card") : target.closest(".discussion-form");
reply = reply.find(".comment-field").val().trim();

if (reply) {
let doctype = $(e.currentTarget).closest(".discussions-parent").attr("data-doctype");
let doctype = target.closest(".discussions-parent").attr("data-doctype");
doctype = doctype ? decodeURIComponent(doctype) : doctype;

let docname = $(e.currentTarget).closest(".discussions-parent").attr("data-docname");
let docname = target.closest(".discussions-parent").attr("data-docname");
docname = docname ? decodeURIComponent(docname) : docname;


frappe.call({
method: "frappe.website.doctype.discussion_topic.discussion_topic.submit_discussion",
args: {
@@ -218,7 +220,8 @@ const submit_discussion = (e) => {
"docname": docname ? docname : "",
"reply": reply,
"title": title,
"topic_name": $(e.currentTarget).closest(".discussion-on-page").attr("data-topic")
"topic_name": target.closest(".discussion-on-page").attr("data-topic"),
"reply_name": reply_name
}
});
}
@@ -252,18 +255,64 @@ const style_avatar_frame = (template) => {
};

const clear_comment_box = () => {
$(".discussion-on-page .comment-field").val("");
$(".discussion-form .comment-field").val("");
$(".cancel-comment").removeClass("show").addClass("hide");
$(".discussion-on-page .comment-field").css("height", "48px");
};

const hide_sidebar = () => {
$(".discussions-sidebar").addClass("hide");
$("#discussion-group").removeClass("hide");
$(".search-field").addClass("hide");
$(".reply").addClass("hide");
};

const back_to_sidebar = () => {
$(".discussions-sidebar").removeClass("hide");
$("#discussion-group").addClass("hide");
$(".discussion-on-page").collapse("hide");
$(".search-field").removeClass("hide");
$(".reply").removeClass("hide");
};

const perform_action = (e) => {
const action = $(e.target).data().action;
const reply_card = $(e.target).closest(".reply-card");

if (action === "edit") {
reply_card.find(".reply-edit-card").removeClass("hide");
reply_card.find(".reply-body").addClass("hide");
reply_card.find(".reply-actions").removeClass("hide");
} else if (action === "delete") {
frappe.call({
method: "frappe.website.doctype.discussion_reply.discussion_reply.delete_message",
args: {
"reply_name": $(e.target).closest(".reply-card").data("reply")
}
});
}
};

const dismiss_reply = (e) => {
const reply_card = $(e.currentTarget).closest(".reply-card");
reply_card.find(".reply-edit-card").addClass("hide");
reply_card.find(".reply-body").removeClass("hide");
reply_card.find(".reply-actions").addClass("hide");
};

const adjust_comment_box = (e) => {
if ($(e.currentTarget).val()) {
$(".cancel-comment").removeClass("hide").addClass("show");
} else {
$(".cancel-comment").removeClass("show").addClass("hide");
}
};

const hide_actions_on_conditions = (template, owner) => {
let $template = $(template);
frappe.session.user != owner && $template.find(".dropdown").addClass("hide");
return $template.prop("outerHTML");
};

const delete_message = (data) => {
$(`[data-reply=${data.reply_name}]`).addClass("hide");
};

+ 29
- 17
frappe/templates/discussions/discussions_section.html View File

@@ -9,25 +9,31 @@

<div class="discussions-header">
<span class="discussion-heading">{{ _(title) }}</span>
{% if topics | length and not single_thread %}
{% include "frappe/templates/discussions/search.html" %}
{% endif %}
{% if topics and not single_thread %}
{% include "frappe/templates/discussions/button.html" %}
{% endif %}
</div>

<div class="card-style thread-card {% if topics | length and not single_thread %} discussions-card {% endif %}
{% if not topics | length %} empty-state {% endif %}">
<div class="">
{% if topics and not single_thread %}

<div class="discussions-sidebar">
{% include "frappe/templates/discussions/search.html" %}
<div class="discussions-sidebar card-style">

{% for topic in topics %}
{% set replies = frappe.get_all("Discussion Reply", {"topic": topic.name})%}
{% include "frappe/templates/discussions/sidebar.html" %}

{% if loop.index != topics | length %}
<div class="card-divider"></div>
{% endif %}

{% endfor %}
</div>

<div class="mr-2" id="discussion-group">
<div class="hide" id="discussion-group">
{% for topic in topics %}
{% include "frappe/templates/discussions/reply_section.html" %}
{% endfor %}
@@ -38,19 +44,25 @@
{% include "frappe/templates/discussions/reply_section.html" %}

{% else %}
<div class="no-discussions">
<img class="icon icon-xl" src="/assets/frappe/icons/timeless/message.svg">
<div class="discussion-heading mt-4 mb-0" style="color: inherit;"> {{ empty_state_title }} </div>
<div class="small mb-6"> {{ empty_state_subtitle }} </div>
{% if frappe.session.user == "Guest" %}
<div class="btn btn-default btn-md mt-3" id="login-from-discussion"> {{ _("Login") }} </div>
{% elif condition is defined and not condition %}
<div class="btn btn-default btn-md mt-3" id="login-from-discussion" data-redirect="{{ redirect_to }}">
{{ button_name }}
<div class="empty-state">
<div>
<img class="icon icon-xl" src="/assets/frappe/icons/timeless/message.svg">
</div>
<div class="empty-state-text">
<div class="empty-state-heading">{{ empty_state_title }}</div>
<div class="course-meta">{{ empty_state_subtitle }}</div>
</div>
<div>
{% if frappe.session.user == "Guest" %}
<div class="btn btn-default btn-md login-from-discussion"> {{ _("Login") }} </div>
{% elif condition is defined and not condition %}
<div class="btn btn-default btn-md login-from-discussion" data-redirect="{{ redirect_to }}">
{{ button_name }}
</div>
{% else %}
{% include "frappe/templates/discussions/button.html" %}
{% endif %}
</div>
{% else %}
{% include "frappe/templates/discussions/button.html" %}
{% endif %}
</div>
{% endif %}
</div>


+ 43
- 7
frappe/templates/discussions/reply_card.html View File

@@ -1,14 +1,50 @@
{% from "frappe/templates/includes/avatar_macro.html" import avatar %}
<div class="reply-card">
<div class="reply-card" data-reply="{{ reply.name }}">
{% set member = frappe.db.get_value("User", reply.owner, ["name", "full_name", "username"], as_dict=True) %}
<div class="d-flex align-items-center small mb-2">
{% if loop.index == 1 or single_thread %}
<div class="reply-header">
{{ avatar(reply.owner) }}
{% endif %}
<a class="button-links {% if loop.index == 1 or single_thread %} ml-2 {% endif %}" {% if get_profile_url %} href="{{ get_profile_url(member.username) }}" {% endif %}>
<a class="button-links topic-author ml-4"
{% if get_profile_url %} href="{{ get_profile_url(member.username) }}" {% endif %}>
{{ member.full_name }}
</a>
<div class="ml-3 frappe-timestamp" data-timestamp="{{ reply.creation }}"> {{ frappe.utils.pretty_date(reply.creation) }} </div>
<div class="ml-2 frappe-timestamp small" data-timestamp="{{ reply.creation }}"> {{ frappe.utils.pretty_date(reply.creation) }} </div>
<div class="reply-actions hide">
<div class="submit-discussion mr-2"> {{ _("Post") }} </div>
<div class="dismiss-reply"> {{ _("Dismiss") }} </div>
</div>
</div>

<div class="reply-body">
{% if frappe.session.user == reply.owner %}
<div class="dropdown">
<svg class="icon icon-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<use xlink:href="#icon-dot-horizontal"></use>
</svg>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<li>
<a class="dropdown-item small" data-action="edit"> {{ _("Edit") }} </a>
</li>
{% if index != 1 %}
<li>
<a class="dropdown-item small" data-action="delete"> {{ _("Delete") }} </a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
<div class="reply-text">{{ frappe.utils.md_to_html(reply.reply) }}</div>
</div>

<div class="reply-edit-card hide">
<div class="form-group">
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control comment-field"
data-fieldtype="Text" data-fieldname="feedback_comments" spellcheck="false">{{ reply.reply }}</textarea>
</div>
</div>
</div>
</div>
<div class="reply-text">{{ frappe.utils.md_to_html(reply.reply) }}</div>
</div>


+ 31
- 22
frappe/templates/discussions/reply_section.html View File

@@ -1,43 +1,52 @@
{% if topic %}

{% set replies = frappe.get_all("Discussion Reply", {"topic": topic.name},
["reply", "owner", "creation"], order_by="creation")%}
["reply", "owner", "creation", "name"], order_by="creation")%}

{% endif %}

<div class="collapse discussion-on-page" data-parent="#discussion-group"
<div class=" {% if not single_thread %} collapse {% endif %} discussion-on-page card-style" data-parent="#discussion-group"
{% if topic %} id="t{{ topic.name }}" data-topic="{{ topic.name }}" {% endif %}>

{% if not single_thread %}
<div class="btn btn-md btn-default ellipsis back">
{{ _("Back") }}
</div>
{% endif %}
<div class="reply-section-header">
{% if not single_thread %}
<div class="back-button">
<svg class="icon icon-md mr-0">
<use class="" href="#icon-left"></use>
</svg>
</div>
{% endif %}

{% if topic and topic.title %}
<div class="discussion-heading p-0">{{ topic.title }}</div>
{% endif %}
{% if topic and topic.title %}
<div class="discussion-heading p-0">{{ topic.title }}</div>
{% endif %}
</div>

{% for reply in replies %}
{% set index = loop.index %}
{% include "frappe/templates/discussions/reply_card.html" %}

{% if loop.index != replies | length %}
<div class="card-divider-dark mb-8"></div>
{% endif %}
{% endfor %}

{% if frappe.session.user == "Guest" or (condition is defined and not condition) %}
<div class="d-flex flex-column align-items-center small">
{{ _("Want to join the discussion?") }}
{% if frappe.session.user == "Guest" %}
<div class="btn btn-default btn-md mt-3 mb-3" id="login-from-discussion">{{ _("Login") }}</div>
{% elif not condition %}
<div class="btn btn-default btn-md mt-3 mb-3" id="login-from-discussion" data-redirect="{{ redirect_to }}">{{ button_name }}
<div class="empty-state">
<div>
<img class="icon icon-xl" src="/assets/frappe/icons/timeless/message.svg">
</div>
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("Want to discuss?") }}</div>
<div class="course-meta">{{ _("Post it here, our mentors will help you out.") }}</div>
</div>
<div>
{% if frappe.session.user == "Guest" %}
<div class="btn btn-default btn-md login-from-discussion"> {{ _("Login") }} </div>
{% elif condition is defined and not condition %}
<div class="btn btn-default btn-md login-from-discussion" data-redirect="{{ redirect_to }}">
{{ button_name }}
</div>
{% endif %}
</div>
{% endif %}
</div>
{% else %}
{% include "frappe/templates/discussions/comment_box.html" %}
{% endif %}

</div>

+ 2
- 9
frappe/templates/discussions/search.html View File

@@ -1,9 +1,2 @@
<div class="form-group">
<div class="control-input-wrapper">
<div class="control-input">
<input type="text" autocomplete="off" class="input-with-feedback form-control search-field"
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="Search {{ title }}"
spellcheck="false"></input>
</div>
</div>
</div>
<input type="text" autocomplete="off" class="search-field" data-fieldtype="Text"
data-fieldname="feedback_comments" placeholder="Search {{ title }}" spellcheck="false"></input>

+ 17
- 12
frappe/templates/discussions/sidebar.html View File

@@ -1,19 +1,24 @@
<div class="sidebar-parent">
<div class="sidebar-topic" data-target="#t{{ topic.name }}" data-toggle="collapse" aria-expanded="false">
{% from "frappe/templates/includes/avatar_macro.html" import avatar %}
{% set creator = frappe.db.get_value("User", topic.owner, ["name", "username", "full_name", "user_image"], as_dict=True) %}

<div class="sidebar-parent" data-target="#t{{ topic.name }}" data-toggle="collapse" aria-expanded="false">
<div class="mr-4">
{{ avatar(creator.name, size="avatar-medium") }}
</div>
<div class="flex-grow-1">
<div class="discussion-topic-title">{{ topic.title }}</div>
<div class="sidebar-info">
{% set creator = frappe.get_doc("User", topic.owner) %}
<span class="reply-author ml-0">
{{ creator.full_name }}
</span>
<span class="small d-flex">
<span class="mr-2 d-flex align-items-center">
<div class="sidebar-topic">
<svg class="icon icon-md m-0 mr-2">
<use class="" href="#icon-reply"></use>
</svg>
<div class="topic-author">{{ creator.full_name }}</div>
<div class="ml-2 frappe-timestamp small" data-timestamp="{{ topic.creation }}"> {{ frappe.utils.pretty_date(topic.creation) }} </div>
<div class="ml-auto">
<span class="d-flex align-items-center">
<img class="mr-1" src="/assets/frappe/icons/timeless/message.svg">
<span class="reply-count">{{ replies | length }}</span>
</span>
<span> {{ frappe.utils.format_date(topic.creation, "dd MMM YYYY") }} </span>
</span>
</div>
</div>
</div>
<div class="card-divider"></div>
</div>

+ 132
- 88
frappe/templates/styles/discussion_style.css View File

@@ -1,25 +1,10 @@
.thread-card {
flex-direction: column;
padding: 1rem;
}

.thread-card .form-control {
background-color: #FFFFFF;
font-size: inherit;
color: inherit;
padding: 0.75rem 1rem;
border-radius: 4px;
resize: none;
}

.modal .comment-field {
height: 300px;
resize: none;
}

.discussion-on-page .comment-field {
height: 48px;
box-shadow: inset 0px 0px 4px rgba(0, 0, 0, 0.2);
padding: 1rem;
}

.modal .cancel-comment {
@@ -31,61 +16,49 @@
}

.cancel-comment {
font-size: 0.75rem;
font-size: var(--text-sm);
margin-right: 0.5rem;
cursor: pointer;
}

.no-discussions {
width: 500px;
margin: 0 auto;
text-align: center;
}

.no-discussions .button {
margin: auto;
}

.discussions-header {
margin: 2.5rem 0 1.25rem;
display: flex;
align-items: center;
}

@media (max-width: 500px) {
.discussions-header {
flex-direction: column;
align-items: inherit;
}
}

.discussions-header .button {
float: right;
}

.discussions-parent .search-field {
background-color: #E2E6E9;
.search-field {
background-image: url(/assets/frappe/icons/timeless/search.svg);
background-repeat: no-repeat;
text-indent: 1.5rem;
background-position: 1rem 0.7rem;
height: 36px;
font-size: 12px;
padding: 0.65rem 0.9rem;
}

.discussions-sidebar {
background-color: #F4F5F6;
padding: 0.75rem;
border-radius: 4px;
background-position: 1rem 0.65rem;
font-size: var(--text-md);
padding: 0.5rem 1rem;
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius-md);
margin-right: 0.5rem;
}

@media (max-width: 550px) {
.discussions-sidebar {
padding: 1rem;
@media (max-width: 500px) {
.search-field {
margin: 0.75rem 0;
}
}

.sidebar-topic {
padding: 0.75rem;
margin: 0.75rem 0;
cursor: pointer;
}

.sidebar-topic[aria-expanded="true"] {
background: #FFFFFF;
border-radius: 4px;
display: flex;
align-items: center;
}

.comment-footer {
@@ -95,23 +68,46 @@
}

.reply-card {
margin-bottom: 2rem;
margin-bottom: 1.5rem;
}

.discussions-parent .collapsing {
transition: height 0s;
.reply-card .dropdown {
float: right;
}

.discussion-topic-title {
color: var(--gray-900);
color: var(--text-color);
font-size: var(--text-lg);
font-weight: 600;
margin-bottom: 0.5rem;
}

.discussion-on-page .topic-title {
display: none;
}

.discussions-sidebar .sidebar-parent:last-child .card-divider {
display: none;
.discussion-on-page {
flex-direction: column;
padding: 1.5rem;
}

.submit-discussion {
cursor: pointer;
}

.reply-body {
background: var(--bg-color);
padding: 1rem;
border-radius: var(--border-radius);
font-size: var(--text-md);
color: var(--text-color);
}

.reply-actions {
display: flex;
align-items: center;
font-size: var(--text-sm);
margin-left: auto;
}

.reply-text h1 {
@@ -130,6 +126,10 @@
font-size: 1rem;
}

.reply-text p {
margin-bottom: 0;
}

.sidebar-info {
margin-top: 0.5rem;
display: flex;
@@ -139,12 +139,11 @@

.discussion-heading {
font-weight: 600;
font-size: 22px;
font-size: var(--text-3xl);
line-height: 146%;
letter-spacing: -0.0175em;
color: var(--gray-900);
margin-bottom: 1rem;
padding: 0 1rem;
color: var(--text-color);
flex-grow: 1;
}

.card-style {
@@ -152,7 +151,7 @@
background: white;
border-radius: 8px;
position: relative;
border: 1px solid var(--gray-200);
box-shadow: var(--shadow-sm);
}

.discussions-card {
@@ -179,48 +178,93 @@
}
}

@media (max-width: 550px) {
.back {
margin-top: 0.5rem;
margin-bottom: 1rem;
.back-button {
margin-right: 1rem;
cursor: pointer;
}

.reply-author {
display: flex;
align-items: center;
margin: 0px 8px;
font-size: var(--text-sm);
line-height: 135%;
color: var(--text-color);
}

.discussions-header .btn {
float: right;
}

.empty-state {
background: var(--control-bg);
border-radius: var(--border-radius-lg);
padding: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}

.empty-state-text {
flex: 1;
margin-left: 1.25rem;
}

.empty-state-heading {
font-size: var(--text-xl);
color: var(--text-color);
font-weight: 600;
}

.sidebar-parent {
display: flex;
align-items: center;
padding: 1.25rem;
cursor: pointer;
}

@media (max-width: 500px) {
.sidebar-parent {
padding: 0.5rem;
}
}

@media (min-width: 550px) {
.back {
display: none;
@media (max-width: 400px) {
.sidebar-parent {
font-size: var(--text-sm);
}
}

.reply-author {
margin: 0px 8px;
font-size: 12px;
line-height: 135%;
color: var(--gray-900);
.topic-author {
color: var(--text-light);
font-weight: 500;
}

.card-divider {
border-top: 1px solid var(--gray-200);
margin-bottom: 1rem;
.reply-section-header {
display: flex;
align-items: center;
margin-bottom: 2.5rem;
}

.card-divider-dark {
border-top: 1px solid var(--gray-300);
.reply-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
}

.empty-state {
background: var(--gray-200);
border: 1px dashed var(--gray-400);
box-sizing: border-box;
border-radius: 8px;
padding: 2.5rem;
.dismiss-reply {
cursor: pointer;
}

.discussions-parent .btn-default {
color: var(--gray-700);
.discussions-sidebar {
flex-direction: column;
}

.discussions-header .btn {
float: right;
.card-divider {
border-top: 1px solid var(--dark-border-color);
margin-bottom: 0;
}

.reply-body .dropdown-menu {
min-width: 7rem;
}

+ 17
- 17
frappe/tests/ui_test_helpers.py View File

@@ -203,39 +203,40 @@ def create_data_for_discussions():

def create_web_page(title, route, single_thread):
web_page = frappe.db.exists("Web Page", {"route": route})
if not web_page:
web_page = frappe.get_doc({
if web_page:
return web_page
web_page = frappe.get_doc({
"doctype": "Web Page",
"title": title,
"route": route,
"published": True
})
web_page.save()

web_page.append("page_blocks", {
"web_template": "Discussions",
"web_template_values": frappe.as_json({
"title": "Discussions",
"cta_title": "New Discussion",
"docname": web_page.name,
"single_thread": single_thread
})
web_page.save()

web_page.append("page_blocks", {
"web_template": "Discussions",
"web_template_values": frappe.as_json({
"title": "Discussions",
"cta_title": "New Discussion",
"docname": web_page.name,
"single_thread": single_thread
})
web_page.save()
})
web_page.save()

return web_page
return web_page.name

def create_topic_and_reply(web_page):
topic = frappe.db.exists("Discussion Topic",{
"reference_doctype": "Web Page",
"reference_docname": web_page.name
"reference_docname": web_page
})

if not topic:
topic = frappe.get_doc({
"doctype": "Discussion Topic",
"reference_doctype": "Web Page",
"reference_docname": web_page.name,
"reference_docname": web_page,
"title": "Test Topic"
})
topic.save()
@@ -274,7 +275,6 @@ def update_child_table(name):

doc.save()


@frappe.whitelist()
def insert_doctype_with_child_table_record(name):
if frappe.db.get_all(name, {'title': 'Test Grid Search'}):


+ 25
- 3
frappe/website/doctype/discussion_reply/discussion_reply.py View File

@@ -1,12 +1,21 @@
# Copyright (c) 2021, FOSS United and contributors
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt

import frappe
from frappe.model.document import Document

class DiscussionReply(Document):
def after_insert(self):

def on_update(self):
frappe.publish_realtime(
event="update_message",
message = {
"reply": frappe.utils.md_to_html(self.reply),
"reply_name": self.name
},
after_commit=True)

def after_insert(self):
replies = frappe.db.count("Discussion Reply", {"topic": self.topic})
topic_info = frappe.get_all("Discussion Topic",
{"name": self.topic},
@@ -37,6 +46,19 @@ class DiscussionReply(Document):
"template": template,
"topic_info": topic_info[0],
"sidebar": sidebar,
"new_topic_template": new_topic_template
"new_topic_template": new_topic_template,
"reply_owner": self.owner
},
after_commit=True)

def after_delete(self):
frappe.publish_realtime(
event="delete_message",
message = {
"reply_name": self.name
},
after_commit=True)

@frappe.whitelist()
def delete_message(reply_name):
frappe.delete_doc("Discussion Reply", reply_name, ignore_permissions=True)

+ 8
- 1
frappe/website/doctype/discussion_topic/discussion_topic.py View File

@@ -8,7 +8,14 @@ class DiscussionTopic(Document):
pass

@frappe.whitelist()
def submit_discussion(doctype, docname, reply, title, topic_name=None):
def submit_discussion(doctype, docname, reply, title, topic_name=None, reply_name=None):

if reply_name:
doc = frappe.get_doc("Discussion Reply", reply_name)
doc.reply = reply
doc.save(ignore_permissions=True)
return

if topic_name:
save_message(reply, topic_name)
return topic_name


Loading…
Cancel
Save