Add table to design elements [SCI-11810]

This commit is contained in:
Anton 2025-04-14 16:07:52 +02:00
parent 8547b61ea4
commit 886a849b06
13 changed files with 200 additions and 188 deletions

View file

@ -1,29 +1,40 @@
# frozen_string_literal: true
class DesignElementsController < ApplicationController
def index
end
def index; end
def test_select
render json: { data: [
['1', 'One'],
['2', 'Two'],
['3', 'Three'],
['4', 'Four'],
['5', 'Five'],
['6', 'Six'],
['7', 'Seven'],
['8', 'Eight'],
['9', 'Nine'],
['10', 'Ten']
%w(1 One),
%w(2 Two),
%w(3 Three),
%w(4 Four),
%w(5 Five),
%w(6 Six),
%w(7 Seven),
%w(8 Eight),
%w(9 Nine),
%w(10 Ten)
].select { |item| item[1].downcase.include?(params[:query].downcase) } }
end
def test_table
render json: {
data: [
{ id: 1, attributes: {name: 'One' } },
{ id: 2, attributes: {name: 'Two' } },
{ id: 3, attributes: {name: 'Three' } },
{ id: 4, attributes: {name: 'Four' } },
{ id: 1, attributes: {
name: 'One',
description: nil,
date: {
value: I18n.l(DateTime.now, format: :default),
value_formatted: I18n.l(DateTime.now, format: :full_date),
editable: true
}
} },
{ id: 2, attributes: { name: 'Two', description: '[@admin~1]', date: { editable: true } } },
{ id: 3,
attributes: { name: 'Three', description: 'Long long long long name Long long long long name Long long long long name Long long long long name',
date: { editable: true } } },
{ id: 4, attributes: { name: 'Four', date: { editable: true } } }
],
meta: {
current_page: 1,

View file

@ -1,89 +1,60 @@
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import DataTable from '../shared/datatable/table.vue';
import DataTable from '../../../vue/shared/datatable/table.vue';
import DescriptionRenderer from '../../../vue/shared/datatable/renderers/description.vue';
import DateRenderer from '../../../vue/shared/datatable/renderers/date.vue';
import { mountWithTurbolinks } from '../helpers/turbolinks.js';
const app = createApp({
data() {
return {
dataUrl: '/design_elements/test_table'
};
},
computed: {
columnDefs() {
const columns = [{
field: 'name',
flex: 1,
headerName: this.i18n.t('projects.index.card.name'),
sortable: true,
cellRenderer: 'NameRenderer'
}]
const columns = [
{
field: 'name',
flex: 1,
headerName: 'Name',
sortable: true
},
{
field: 'description',
flex: 1,
headerName: 'Description',
sortable: true,
cellRenderer: DescriptionRenderer
},
{
field: 'date',
flex: 1,
headerName: 'Date',
sortable: true,
cellRenderer: DateRenderer,
cellRendererParams: {
placeholder: 'Add date',
field: 'date',
mode: 'datetime',
emptyPlaceholder: 'No date',
emitAction: 'updateDate'
}
}
];
return columns;
},
viewRenders() {
return [
{ type: 'table' },
{ type: 'cards' }
];
return [];
},
toolbarActions() {
const left = [];
if (this.createUrl && this.currentViewMode !== 'archived') {
left.push({
name: 'create',
icon: 'sn-icon sn-icon-new-task',
label: this.i18n.t('projects.index.new'),
type: 'emit',
path: this.createUrl,
buttonStyle: 'btn btn-primary'
});
}
if (this.createFolderUrl) {
left.push({
name: 'create_folder',
icon: 'sn-icon sn-icon-folder',
label: this.i18n.t('projects.index.new_folder'),
type: 'emit',
path: this.createFolderUrl,
buttonStyle: 'btn btn-light'
});
}
return {
left,
left: [],
right: []
};
},
filters() {
const filters = [
{
key: 'query',
type: 'Text'
},
{
key: 'created_at',
type: 'DateRange',
label: this.i18n.t('filters_modal.created_on.label')
}
];
if (this.currentViewMode === 'archived') {
filters.push({
key: 'archived_at',
type: 'DateRange',
label: this.i18n.t('filters_modal.archived_on.label')
});
}
filters.push({
key: 'members',
type: 'Select',
optionsUrl: this.usersFilterUrl,
optionRenderer: this.usersFilterRenderer,
labelRenderer: this.usersFilterRenderer,
label: this.i18n.t('projects.index.filters_modal.members.label'),
placeholder: this.i18n.t('projects.index.filters_modal.members.placeholder')
});
filters.push({
key: 'folder_search',
type: 'Checkbox',
label: this.i18n.t('projects.index.filters_modal.folders.label')
});
const filters = [];
return filters;
}

View file

@ -75,7 +75,7 @@
import RowMenuRenderer from '../shared/datatable/row_menu_renderer.vue';
import CardSelectorMixin from '../shared/datatable/mixins/card_selector.js';
import workflowImgMixin from './workflow_img_mixin.js';
import Description from './renderers/description.vue';
import Description from '../shared/datatable/renderers/description.vue';
export default {
name: 'ProjectCard',

View file

@ -61,7 +61,7 @@
import axios from '../../packs/custom_axios.js';
import DataTable from '../shared/datatable/table.vue';
import DescriptionRenderer from './renderers/description.vue';
import DescriptionRenderer from '../shared/datatable/renderers/description.vue';
import ConfirmationModal from '../shared/confirmation_modal.vue';
import CompletedTasksRenderer from './renderers/completed_tasks.vue';
import NameRenderer from './renderers/name.vue';

View file

@ -24,6 +24,7 @@
@archive="archive"
@restore="restore"
@duplicate="duplicate"
@updateDueDate="updateDueDate"
@editTags="editTags"/>
<TagsModal v-if="tagsModalObject"
@ -60,7 +61,7 @@ import ConfirmationModal from '../shared/confirmation_modal.vue';
import NameRenderer from './renderers/name.vue';
import ResultsRenderer from './renderers/results.vue';
import StatusRenderer from './renderers/status.vue';
import DueDateRenderer from './renderers/due_date.vue';
import DueDateRenderer from '../shared/datatable/renderers/date.vue';
import DesignatedUsers from './renderers/designated_users.vue';
import TagsModal from './modals/tags.vue';
import TagsRenderer from './renderers/tags.vue';
@ -134,6 +135,13 @@ export default {
headerName: this.i18n.t('experiments.table.column.due_date_html'),
sortable: true,
cellRenderer: DueDateRenderer,
cellRendererParams: {
placeholder: this.i18n.t('my_modules.details.no_due_date_placeholder'),
field: 'due_date_cell',
mode: 'datetime',
emptyPlaceholder: this.i18n.t('my_modules.details.no_due_date'),
emitAction: 'updateDueDate'
},
minWidth: 200
},
{
@ -275,6 +283,25 @@ export default {
}
},
methods: {
updateDueDate(value, params) {
axios.put(params.data.urls.update_due_date, {
my_module: {
due_date: this.formatDate(value)
}
}).then(() => {
this.updateTable();
});
},
formatDate(date) {
if (!(date instanceof Date)) return null;
const y = date.getFullYear();
const m = date.getMonth() + 1;
const d = date.getDate();
const hours = date.getHours();
const mins = date.getMinutes();
return `${y}/${m}/${d} ${hours}:${mins}`;
},
updateTable() {
this.tagsModalObject = null;
this.newModalOpen = false;

View file

@ -1,81 +0,0 @@
<template>
<div class="flex relative items-center gap-2">
<DateTimePicker
v-if="this.params.data.urls.update_due_date"
class="borderless-input -mt-[1px]"
:defaultValue="dueDate"
@change="updateDueDate"
mode="datetime"
:placeholder="i18n.t('my_modules.details.no_due_date_placeholder')"
:customIcon="customIcon"
:clearable="true"/>
<template v-else-if="params.data.due_date_formatted ">
<i :class="customIcon || 'sn-icon sn-icon-calendar'"></i>
{{ params.data.due_date_formatted }}
</template>
<template v-else>
{{ i18n.t('my_modules.details.no_due_date') }}
</template>
</div>
</template>
<script>
import axios from '../../../packs/custom_axios.js';
import DateTimePicker from '../../shared/date_time_picker.vue';
export default {
name: 'DueDateRenderer',
components: {
DateTimePicker
},
props: {
params: {
required: true
}
},
data() {
return {
dueDate: null
};
},
created() {
this.dueDate = new Date(this.params.data.due_date?.replace(/([^!\s])-/g, '$1/'));
},
computed: {
customIcon() {
if (this.params.data.due_date_status === 'overdue') {
return 'sn-icon sn-icon-alert-warning text-sn-delete-red';
}
if (this.params.data.due_date_status === 'one_day_prior') {
return 'sn-icon sn-icon-alert-warning text-sn-alert-brittlebush';
}
return null;
}
},
methods: {
updateDueDate(value) {
this.dueDate = value;
axios.put(this.params.data.urls.update_due_date, {
my_module: {
due_date: this.formatDate(value)
}
}).then(() => {
this.params.dtComponent.updateTable();
});
},
formatDate(date) {
if (!(date instanceof Date)) return null;
const y = date.getFullYear();
const m = date.getMonth() + 1;
const d = date.getDate();
const hours = date.getHours();
const mins = date.getMinutes();
return `${y}/${m}/${d} ${hours}:${mins}`;
}
}
};
</script>

View file

@ -0,0 +1,51 @@
<template>
<div class="flex relative items-center gap-2">
<DateTimePicker
v-if="this.params.data[this.params.field].editable"
class="borderless-input -mt-[1px]"
:defaultValue="date"
@change="updateDate"
:mode="this.params.mode || 'datetime'"
:placeholder="this.params.placeholder"
:customIcon="this.params.data[this.params.field].icon"
:clearable="true"/>
<template v-else-if="this.params.data[this.params.field].value_formatted">
<i :class="this.params.data[this.params.field].icon || 'sn-icon sn-icon-calendar'"></i>
{{ this.params.data[this.params.field].value_formatted }}
</template>
<template v-else>
{{ this.params.emptyPlaceholder }}
</template>
</div>
</template>
<script>
import DateTimePicker from '../../date_time_picker.vue';
export default {
name: 'dateRenderer',
components: {
DateTimePicker
},
props: {
params: {
required: true
}
},
data() {
return {
date: null
};
},
created() {
this.date = new Date(this.params.data[this.params.field].value?.replace(/([^!\s])-/g, '$1/'));
},
methods: {
updateDate(value) {
this.params.dtComponent.$emit(this.params.emitAction, value, this.params);
this.date = value;
}
}
};
</script>

View file

@ -1,12 +1,12 @@
<template>
<template v-if="params.dtComponent.currentViewRender === 'table'">
<div class="group relative flex items-center group-hover:marker text-xs h-full w-full leading-[unset]">
<div class="flex gap-2 w-full items-center text-sm leading-[unset]">
<div ref="descripitonBox" class="flex gap-2 w-full items-center text-sm leading-[unset]">
<span class="cursor-pointer line-clamp-1 leading-[unset]"
@click.stop="showDescriptionModal"
v-html="params.data.sa_description">
@click.stop="showDescriptionModal">
{{ params.data.description }}
</span>
<span @click.stop="showDescriptionModal" class="text-sn-blue cursor-pointer shrink-0 inline-block text-sm">
<span @click.stop="showDescriptionModal" class="text-sn-blue cursor-pointer shrink-0 inline-block text-sm leading-[unset]">
{{ i18n.t('experiments.card.more') }}
</span>
</div>
@ -14,13 +14,13 @@
</template>
<template v-else>
<div class="group relative flex items-center group-hover:marker text-xs h-full w-full">
<div class="flex gap-2 w-full items-end text-xs">
<div ref="descripitonBox" class="flex gap-2 w-full items-end text-xs">
<span v-if="shouldTruncateText"
class="cursor-pointer grow line-clamp-2"
@click.stop="showDescriptionModal"
v-html="params.data.sa_description">
@click.stop="showDescriptionModal">
{{ params.data.description }}
</span>
<span v-else class="grow" v-html="params.data.sa_description"></span>
<span v-else class="grow">{{ params.data.description }}</span>
<span v-if="shouldTruncateText" @click.stop="showDescriptionModal" class="text-sn-blue cursor-pointer shrink-0 inline-block text-xs">
{{ i18n.t('experiments.card.more') }}
</span>
@ -37,6 +37,11 @@ export default {
required: true
}
},
mounted() {
this.$nextTick(() => {
window.renderElementSmartAnnotations(this.$refs.descripitonBox, 'span');
});
},
computed: {
shouldTruncateText() {
return this.params.data.description?.length > 60;

View file

@ -249,9 +249,7 @@ export default {
const columns = this.columnDefs.map((column) => ({
...column,
minWidth: column.minWidth || 110,
cellRendererParams: {
dtComponent: this
},
cellRendererParams: { ...column.cellRendererParams, ...{ dtComponent: this } },
pinned: (column.field === 'name' || column.field === 'name_hash' ? 'left' : null),
comparator: () => null
}));

View file

@ -22,10 +22,11 @@ module Lists
tags_html
comments
due_date_formatted
due_date_cell
permissions
default_public_user_role_id
team
)
).freeze
def attributes(_options = {})
ATTRIBUTES.index_with do |attribute|
@ -67,13 +68,9 @@ module Lists
provisioning_status: provisioning_status_my_module_url(object)
}
if can_manage_project_users?(object.experiment.project)
urls_list[:update_access] = access_permissions_my_module_path(object)
end
urls_list[:update_access] = access_permissions_my_module_path(object) if can_manage_project_users?(object.experiment.project)
if can_update_my_module_due_date?(object)
urls_list[:update_due_date] = my_module_path(object, user, format: :json)
end
urls_list[:update_due_date] = my_module_path(object, user, format: :json) if can_update_my_module_due_date?(object)
urls_list
end
@ -86,6 +83,19 @@ module Lists
I18n.l(object.due_date, format: :full_date) if object.due_date
end
def due_date_cell
{
value: due_date,
value_formatted: due_date_formatted,
editable: can_update_my_module_due_date?(object),
icon: (if object.is_one_day_prior? && !object.completed?
'sn-icon sn-icon-alert-warning text-sn-alert-brittlebush'
elsif object.is_overdue? && !object.completed?
'sn-icon sn-icon-alert-warning text-sn-delete-red'
end)
}
end
def due_date_status
if (archived || object.completed?) && object.due_date
return :ok

View file

@ -0,0 +1,17 @@
<div>
<h1>Table</h1>
<div id="table" class="h-[500px]">
<data-table
:column-defs="columnDefs"
table-id="TestDataTable"
:data-url="dataUrl"
:reloading-table="reloadingTable"
:toolbar-actions="toolbarActions"
:actions-url="actionsUrl"
scroll-mode="infinite"
:filters="filters"
></data-table>
</div>
</div>
<%= javascript_include_tag 'vue_design_system_table' %>

View file

@ -10,6 +10,8 @@
end
%>
<%= render partial: 'table' %>
<%= render partial: 'radio' %>
<%= render partial: 'inputs', locals: {icons_list: icons_list} %>

View file

@ -71,7 +71,8 @@ const entryList = {
vue_form_show: './app/javascript/packs/vue/forms_show.js',
vue_form_table: './app/javascript/packs/vue/forms_table.js',
vue_my_module_assigned_items: './app/javascript/packs/vue/my_module_assigned_items.js',
vue_design_system_inputs: './app/javascript/packs/vue/design_system/inputs.js'
vue_design_system_inputs: './app/javascript/packs/vue/design_system/inputs.js',
vue_design_system_table: './app/javascript/packs/vue/design_system/table.js'
};
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949