diff --git a/Gruntfile.js b/Gruntfile.js index 96ace8aafa..87c89e0e52 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -205,6 +205,15 @@ module.exports = function (grunt) { ], dest: paths.adminJsLib }, + { + expand: true, + cwd: 'node_modules/vue-i18n/dist', + src: [ + // not using runtime 'cos we need the messages compiler + 'vue-i18n.global.js' + ], + dest: paths.adminJsLib + }, ], }, }, diff --git a/conf/schema/pg.sql b/conf/schema/pg.sql deleted file mode 100644 index 59eca9824c..0000000000 --- a/conf/schema/pg.sql +++ /dev/null @@ -1,104 +0,0 @@ -CREATE TABLE users ( - id VARCHAR(50) NOT NULL PRIMARY KEY, - email VARCHAR(255) NOT NULL, - verified BOOLEAN NOT NULL DEFAULT FALSE, - staff BOOLEAN NOT NULL DEFAULT FALSE, - active BOOLEAN NOT NULL DEFAULT TRUE, - allow_messaging BOOLEAN NOT NULL DEFAULT TRUE, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_login TIMESTAMP NULL, - password VARCHAR(255) NULL, - is_legacy BOOLEAN NOT NULL DEFAULT FALSE -); - -CREATE UNIQUE INDEX users_email ON users (email); - -CREATE TABLE openid_association ( - id VARCHAR(50) NOT NULL, - openid_url VARCHAR(255) NOT NULL, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY(id, openid_url) -); - -ALTER TABLE openid_association ADD CONSTRAINT openid_association_id FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE; - -CREATE TABLE oauth2_association ( - id VARCHAR(50) NOT NULL, - provider_id VARCHAR(255) NOT NULL, - provider VARCHAR(255) NOT NULL, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY(id, provider_id, provider) -); - -ALTER TABLE oauth2_association ADD CONSTRAINT oauth2_association_id FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE; - -CREATE TABLE token ( - id VARCHAR(50) NOT NULL, - token VARCHAR(255) NOT NULL PRIMARY KEY, - expires TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - is_sign_up BOOLEAN NOT NULL DEFAULT FALSE -); - -ALTER TABLE token ADD CONSTRAINT token_profile_id FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE; - -CREATE TABLE moved_pages( - original_path_sha1 CHAR(40) PRIMARY KEY, - original_path TEXT NOT NULL, - new_path TEXT NOT NULL, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE research_guide ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - path VARCHAR(255) DEFAULT NULL, - picture VARCHAR(255) DEFAULT NULL, - virtual_unit VARCHAR(255) DEFAULT NULL, - description TEXT, - css TEXT, - active BOOLEAN NOT NULL DEFAULT TRUE, - default_page INTEGER NULL -); - -ALTER TABLE research_guide ADD CONSTRAINT research_guide_path_unique UNIQUE(path); - -CREATE TABLE research_guide_page ( - id SERIAL PRIMARY KEY, - research_guide_id INTEGER NOT NULL, - name TEXT NOT NULL, - layout VARCHAR(45) DEFAULT NULL, - content TEXT, - path VARCHAR(45) DEFAULT NULL, - position VARCHAR(45) DEFAULT NULL, - description TEXT, - params VARCHAR(255) DEFAULT NULL -); - -ALTER TABLE research_guide_page ADD CONSTRAINT research_guide_page_id FOREIGN KEY (research_guide_id) REFERENCES research_guide (id) ON DELETE CASCADE; -ALTER TABLE research_guide_page ADD CONSTRAINT research_guide_path_guide_id UNIQUE (research_guide_id, path); - -CREATE TABLE feedback ( - id CHAR(10) NOT NULL PRIMARY KEY, - user_id VARCHAR(50) DEFAULT NULL, - name VARCHAR (255) DEFAULT NULL, - email VARCHAR (255) DEFAULT NULL, - text TEXT, - type VARCHAR (10) DEFAULT NULL, - copy BOOLEAN DEFAULT FALSE, - context TEXT, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated TIMESTAMP NULL, - mode VARCHAR(10) NOT NULL -); - -CREATE TABLE cypher_queries ( - id CHAR(10) NOT NULL PRIMARY KEY , - user_id VARCHAR(50) DEFAULT NULL, - name VARCHAR(255) NOT NULL, - query TEXT NOT NULL, - description TEXT, - public BOOLEAN DEFAULT FALSE, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated TIMESTAMP NULL -); - diff --git a/etc/db_migrations/20240807_add_entity_meta_tables.sql b/etc/db_migrations/20240807_add_entity_meta_tables.sql new file mode 100644 index 0000000000..a16f249480 --- /dev/null +++ b/etc/db_migrations/20240807_add_entity_meta_tables.sql @@ -0,0 +1,35 @@ +BEGIN TRANSACTION; + +CREATE TABLE entity_type_meta( + entity_type VARCHAR(50) NOT NULL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description TEXT, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP +); + +CREATE INDEX entity_type_meta_entity_type ON entity_type_meta(entity_type); + +CREATE TABLE field_meta( + entity_type VARCHAR(50) NOT NULL, + id VARCHAR(50) NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT, + usage VARCHAR(50), + category VARCHAR(50), + default_val TEXT, + see_also TEXT[], + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP, + PRIMARY KEY (entity_type, id), + CONSTRAINT field_meta_entity_type + FOREIGN KEY (entity_type) + REFERENCES entity_type_meta (entity_type) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX field_meta_entity_type ON field_meta(entity_type); +CREATE INDEX field_meta_id ON field_meta(id); + +COMMIT; diff --git a/modules/admin/app/assets/css/admin/_admin.scss b/modules/admin/app/assets/css/admin/_admin.scss index d1a96563e5..05f1991331 100644 --- a/modules/admin/app/assets/css/admin/_admin.scss +++ b/modules/admin/app/assets/css/admin/_admin.scss @@ -80,11 +80,11 @@ body > .admin-content > .flash { background-color: $gray-100; } } +} - .markdown-helper { - font-style: italic; - cursor: help; - } +.markdown-helper { + font-style: italic; + cursor: pointer; } .inline-remove > a:hover { @@ -92,6 +92,24 @@ body > .admin-content > .flash { cursor: pointer; } +.form-legend { + font-size: $font-size-base; + + dl { + display: flex; + flex-wrap: wrap; + + dt { + @include make-col(3); + font-weight: bold; + } + + dd { + @include make-col(9); + } + } +} + .address-form, .concept-description-form { background-color: $gray-100; @@ -127,8 +145,11 @@ body > .admin-content > .flash { padding-top: $margin-md; } -.add-address { +.add-address, +.add-concept +{ float: right; + margin-bottom: $margin-md; } .admin-help-notice { @@ -315,12 +336,18 @@ pre.code-format { margin-bottom: $margin-md; } -.add-description { - margin-bottom: $margin-md; - @include clearfix(); - a { - float: right; +.control-elements { + .add-inline-element:not(.btn) { + display: block; + background-color: $gray-100; + border-bottom: 1px solid darken($gray-100, 5%); + padding: 0 $margin-xs; + color: $gray-700; + } + .add-inline-element:hover:not(.btn) { + background-color: darken($gray-100, 5%); } + } .inline-element-block-controls { @@ -436,6 +463,20 @@ pre.code-format { } } +// Metadata validation +.metadata-validation { + summary { + font-size: $font-size-lg; + @extend .alert, .alert-warning; + } + .validation-errors { + @extend .alert, .alert-danger; + } + .validation-warnings { + @extend .alert, .alert-warning; + } +} + // Vue Single-page applications %expanding-row { display: flex; diff --git a/modules/admin/app/assets/css/dmeditor.scss b/modules/admin/app/assets/css/dmeditor.scss index 179e9a1ada..addeafbddb 100644 --- a/modules/admin/app/assets/css/dmeditor.scss +++ b/modules/admin/app/assets/css/dmeditor.scss @@ -12,3 +12,30 @@ $active-table-row: #e7f1ff; overflow: auto; flex-basis: 0; } + +.section { + background-color: $gray-100; + font-weight: bold; +} + +.fm-list td.fm-actions { + width: 5rem; +} + +.fm-see-also a + a { + display: inline-block; +} + +.fm-usage { + white-space: nowrap; + +} + +.fm-list td:last-child a { + margin-left: $margin-xs; +} + +#delete-metadata { + margin-right: auto; +} + diff --git a/modules/admin/app/assets/js/admin.js b/modules/admin/app/assets/js/admin.js index 350c6c4d38..c8cb3b104f 100644 --- a/modules/admin/app/assets/js/admin.js +++ b/modules/admin/app/assets/js/admin.js @@ -188,7 +188,7 @@ jQuery(function ($) { event.preventDefault(); var container = $(event.target).closest(".inline-formset"); - var set = container.children(".inline-element-list"); + var set = container.children(".control-elements").children(".inline-element-list").first(); var prefix = container.data("prefix"); if (!prefix) { throw "No prefix found for formset"; @@ -212,6 +212,10 @@ jQuery(function ($) { // And a help popover addPopover($elem.find("textarea,input")); + + // Focus the first input + $elem.find("input,textarea").first().focus(); + }).on("click", ".remove-inline-element", function (event) { event.preventDefault(); $(this) diff --git a/modules/admin/app/assets/js/datasets/components/_manager-convert.vue b/modules/admin/app/assets/js/datasets/components/_manager-convert.vue index 3f180bf347..062ead04e0 100644 --- a/modules/admin/app/assets/js/datasets/components/_manager-convert.vue +++ b/modules/admin/app/assets/js/datasets/components/_manager-convert.vue @@ -20,7 +20,6 @@ import _takeWhile from 'lodash/takeWhile'; import _find from 'lodash/find'; import {DataTransformation} from "../types"; - let initialConvertState = function (config) { return { ingesting: {}, diff --git a/modules/admin/app/assets/js/dmeditor/__mocks__/api.ts b/modules/admin/app/assets/js/dmeditor/__mocks__/api.ts index 83edd95ea5..df03adcec5 100644 --- a/modules/admin/app/assets/js/dmeditor/__mocks__/api.ts +++ b/modules/admin/app/assets/js/dmeditor/__mocks__/api.ts @@ -1,4 +1,5 @@ import { + EntityType, EntityTypeMetadata, EntityTypeMetadataInfo, FieldMetadata, @@ -17,9 +18,9 @@ export default class EntityTypeMetadataApi { this.etData = { Country: { entityType: "Country", - name: "Country", - description: "Country description", - created: "2021-06-01" + name: "Country", + description: "Country description", + created: "2021-06-01" } }; @@ -31,6 +32,7 @@ export default class EntityTypeMetadataApi { name: "History", description: "Test", usage: "mandatory", + defaultVal: null, seeAlso: ["seeAlso"], created: "2021-06-01" } @@ -39,34 +41,34 @@ export default class EntityTypeMetadataApi { } - list(): Promise> { + list(): Promise> { return Promise.resolve(this.etData); } - get(entityType: string): Promise { + get(entityType: EntityType): Promise { return Promise.resolve(this.etData[entityType] || null); } - save(entityType: string, data: EntityTypeMetadataInfo): Promise { + save(entityType: EntityType, data: EntityTypeMetadataInfo): Promise { return Promise.resolve({ entityType, ...data } as EntityTypeMetadata); } - delete(entityType: string): Promise { + delete(entityType: EntityType): Promise { return Promise.resolve(true); } - listFields(entityType?: string): Promise> { + listFields(entityType?: EntityType): Promise> { return Promise.resolve(this.fData); } - getField(entityType: string, id: string): Promise { + getField(entityType: EntityType, id: string): Promise { return Promise.resolve(this.fData[entityType].find(f => f.id === id) || null); } - saveField(entityType: string, id: string, data: FieldMetadataInfo): Promise { + saveField(entityType: EntityType, id: string, data: FieldMetadataInfo): Promise { return Promise.resolve({ entityType, id, @@ -74,7 +76,7 @@ export default class EntityTypeMetadataApi { } as FieldMetadata); } - deleteField(entityType: string, id: string): Promise { + deleteField(entityType: EntityType, id: string): Promise { // remove the item from fData: this.fData[entityType] = this.fData[entityType].filter(f => f.id !== id); return Promise.resolve(true); @@ -83,7 +85,7 @@ export default class EntityTypeMetadataApi { templates(): Promise { return Promise.resolve({ Country: { - "" : ["history"] + "_" : ["history"] } } as FieldMetadataTemplates); } diff --git a/modules/admin/app/assets/js/dmeditor/api.ts b/modules/admin/app/assets/js/dmeditor/api.ts index e7a92aba19..9c2bfc3179 100644 --- a/modules/admin/app/assets/js/dmeditor/api.ts +++ b/modules/admin/app/assets/js/dmeditor/api.ts @@ -1,5 +1,6 @@ import axios from "axios"; import { + EntityType, EntityTypeMetadata, EntityTypeMetadataInfo, FieldMetadata, FieldMetadataInfo, FieldMetadataTemplates, @@ -30,36 +31,36 @@ export default class EntityTypeMetadataApi { withCredentials: true, }).then(r => r.data); } - list(): Promise> { - return EntityTypeMetadataApi.call>(this.service.list(), {}); + list(): Promise> { + return EntityTypeMetadataApi.call>(this.service.list(), {}); } - get(entityType: string): Promise> { - return EntityTypeMetadataApi.call>(this.service.list(), {}, {entityType}); + get(entityType: EntityType): Promise> { + return EntityTypeMetadataApi.call>(this.service.list(), {}, {entityType}); } - save(entityType: string, data: EntityTypeMetadataInfo): Promise { + save(entityType: EntityType, data: EntityTypeMetadataInfo): Promise { return EntityTypeMetadataApi.call(this.service.save(entityType), data); } - delete(entityType: string): Promise { + delete(entityType: EntityType): Promise { return EntityTypeMetadataApi.call(this.service.delete(entityType)); } - listFields(entityType?: string): Promise> { + listFields(entityType?: EntityType): Promise> { let params = entityType ? {entityType} : {}; - return EntityTypeMetadataApi.call>(this.service.listFields(), {}, params) + return EntityTypeMetadataApi.call>(this.service.listFields(), {}, params) } - getField(entityType: string, id: string): Promise { + getField(entityType: EntityType, id: string): Promise { return EntityTypeMetadataApi.call(this.service.getField(entityType, id)); } - saveField(entityType: string, id: string, data: FieldMetadataInfo): Promise { + saveField(entityType: EntityType, id: string, data: FieldMetadataInfo): Promise { return EntityTypeMetadataApi.call(this.service.saveField(entityType, id), data); } - deleteField(entityType: string, id: string): Promise { + deleteField(entityType: EntityType, id: string): Promise { return EntityTypeMetadataApi.call(this.service.deleteField(entityType, id)); } diff --git a/modules/admin/app/assets/js/dmeditor/app.spec.ts b/modules/admin/app/assets/js/dmeditor/app.spec.ts index 279bfb3ada..563deec623 100644 --- a/modules/admin/app/assets/js/dmeditor/app.spec.ts +++ b/modules/admin/app/assets/js/dmeditor/app.spec.ts @@ -1,10 +1,14 @@ -import {mount, flushPromises} from '@vue/test-utils'; +import {mount, flushPromises,config} from '@vue/test-utils'; import App from './app.vue'; import ListEt from './components/_list-et.vue'; import EntityTypeMetadataApi from "./api"; jest.mock('./api'); +config.global.mocks = { + '$t': (msg) => msg, // return i18n key +} + const defaultInit = { props: { service: {}, @@ -55,7 +59,9 @@ test('deleting a field', async () => { expect(wrapper.find(".fm-list").exists()).toBe(true); expect(wrapper.find("#fm-Country-history").exists()).toBe(true); - await wrapper.find("#fm-Country-history .fm-delete").trigger("click"); + await wrapper.find("#fm-Country-history .fm-edit").trigger("click"); + expect(wrapper.find("#delete-metadata").exists()).toBe(true); + await wrapper.find("#delete-metadata").trigger("click"); expect(wrapper.find(".confirm-delete-field-metadata").exists()).toBe(true); await wrapper.find(".confirm-delete-field-metadata button.accept").trigger("click"); await flushPromises(); diff --git a/modules/admin/app/assets/js/dmeditor/app.vue b/modules/admin/app/assets/js/dmeditor/app.vue index d1947272cd..9f90a1bd61 100644 --- a/modules/admin/app/assets/js/dmeditor/app.vue +++ b/modules/admin/app/assets/js/dmeditor/app.vue @@ -1,6 +1,9 @@ - - diff --git a/modules/admin/app/assets/js/dmeditor/components/_modal-fm-editor.vue b/modules/admin/app/assets/js/dmeditor/components/_modal-fm-editor.vue index 447b9cf9b1..25a5bdbd5f 100644 --- a/modules/admin/app/assets/js/dmeditor/components/_modal-fm-editor.vue +++ b/modules/admin/app/assets/js/dmeditor/components/_modal-fm-editor.vue @@ -1,29 +1,34 @@