From 41b56eaf56b11c22a0865827bf32d79de0345c55 Mon Sep 17 00:00:00 2001 From: Svilen Velikov <51084653+svilenvelikov@users.noreply.github.com> Date: Mon, 23 Jan 2023 18:15:32 +0200 Subject: [PATCH] GDB-7785 Introduce share saved query action (#51) * GDB-7785 Introduce share saved query action ## What This MR introduces support for sharing of saved query through a link. ## Why GDB workbench supports functionality for sharing of saved queries through a specially crafted link. ## How * Introduced a new action button for each saved query which opens a window with a field containing the share link. The copy link button writes the link into the clipboard and closes the dialog. * Implemented tests. * Make the share link field readonly in order to prevent modifications by the user. --- .../delete-saved-query.spec.cy.ts | 0 .../edit-saved-query.spec.cy.ts | 0 .../saved-query/share-saved-query.cpec.cy.ts | 36 +++++ cypress/steps/yasqe-steps.ts | 24 ++++ .../src/components.d.ts | 58 ++++++++ .../ontotext-dialog-web-component.scss | 91 ++++++++++++ .../ontotext-dialog-web-component.tsx | 37 +++++ .../ontotext-dialog-web-component/readme.md | 30 ++++ .../ontotext-yasgui-web-component.tsx | 55 ++++++++ .../ontotext-yasgui-web-component/readme.md | 21 +-- .../components/saved-queries-popup/readme.md | 1 + .../saved-queries-popup.tsx | 22 ++- .../share-saved-query-dialog/readme.md | 44 ++++++ .../share-saved-query-dialog.scss | 14 ++ .../share-saved-query-dialog.tsx | 129 ++++++++++++++++++ .../src/css/_common.scss | 60 ++++++++ .../src/i18n/locale-en.json | 4 +- .../src/i18n/locale-fr.json | 4 +- .../src/models/model.ts | 7 + .../src/pages/actions/main.js | 20 +++ 20 files changed, 642 insertions(+), 15 deletions(-) rename cypress/e2e/{editor-actions => saved-query}/delete-saved-query.spec.cy.ts (100%) rename cypress/e2e/{editor-actions => saved-query}/edit-saved-query.spec.cy.ts (100%) create mode 100644 cypress/e2e/saved-query/share-saved-query.cpec.cy.ts create mode 100644 ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/ontotext-dialog-web-component.scss create mode 100644 ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/ontotext-dialog-web-component.tsx create mode 100644 ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/readme.md create mode 100644 ontotext-yasgui-web-component/src/components/share-saved-query-dialog/readme.md create mode 100644 ontotext-yasgui-web-component/src/components/share-saved-query-dialog/share-saved-query-dialog.scss create mode 100644 ontotext-yasgui-web-component/src/components/share-saved-query-dialog/share-saved-query-dialog.tsx diff --git a/cypress/e2e/editor-actions/delete-saved-query.spec.cy.ts b/cypress/e2e/saved-query/delete-saved-query.spec.cy.ts similarity index 100% rename from cypress/e2e/editor-actions/delete-saved-query.spec.cy.ts rename to cypress/e2e/saved-query/delete-saved-query.spec.cy.ts diff --git a/cypress/e2e/editor-actions/edit-saved-query.spec.cy.ts b/cypress/e2e/saved-query/edit-saved-query.spec.cy.ts similarity index 100% rename from cypress/e2e/editor-actions/edit-saved-query.spec.cy.ts rename to cypress/e2e/saved-query/edit-saved-query.spec.cy.ts diff --git a/cypress/e2e/saved-query/share-saved-query.cpec.cy.ts b/cypress/e2e/saved-query/share-saved-query.cpec.cy.ts new file mode 100644 index 00000000..ba2eddab --- /dev/null +++ b/cypress/e2e/saved-query/share-saved-query.cpec.cy.ts @@ -0,0 +1,36 @@ +import {YasqeSteps} from "../../steps/yasqe-steps"; +import {QueryStubs} from "../../stubs/query-stubs"; +import ActionsPageSteps from "../../steps/actions-page-steps"; + +describe('Share saved query action', () => { + beforeEach(() => { + QueryStubs.stubDefaultQueryResponse(); + // Given I have opened a page with the yasgui + // And there is an open tab with sparql query in it + ActionsPageSteps.visit(); + }); + + it('Should be able to get shareable link for any saved query', () => { + // Given I have opened the saved queries popup + YasqeSteps.showSavedQueries(); + YasqeSteps.getSavedQueriesPopup().should('be.visible'); + // When I click on share query button on a particular query + YasqeSteps.shareSavedQuery(0); + // Then I expect a dialog with the shareable link to appear + YasqeSteps.getShareSavedQueryDialog().should('be.visible'); + YasqeSteps.getShareSavedQueryDialogTitle().should('contain', 'Copy URL to clipboard'); + YasqeSteps.getShareSavedQueryLink().should('have.value', 'http://localhost:3333/pages/actions?savedQueryName=Add%20statements'); + // When I cancel operation + YasqeSteps.closeShareSavedQueryDialog(); + // Then I expect that the dialog should be closed + YasqeSteps.getShareSavedQueryDialog().should('not.exist'); + // When I click on share query button on a particular query + YasqeSteps.showSavedQueries(); + YasqeSteps.shareSavedQuery(0); + // And I click the copy button + YasqeSteps.copySavedQueryShareLink(); + // Then I expect that the share link is copied in the clipboard + YasqeSteps.getShareSavedQueryDialog().should('not.exist'); + ActionsPageSteps.getSaveQueryPayload().should('have.value', 'http://localhost:3333/pages/actions?savedQueryName=Add%20statements'); + }); +}); diff --git a/cypress/steps/yasqe-steps.ts b/cypress/steps/yasqe-steps.ts index 1007b43c..4dab9f55 100644 --- a/cypress/steps/yasqe-steps.ts +++ b/cypress/steps/yasqe-steps.ts @@ -144,6 +144,10 @@ export class YasqeSteps { this.getSavedQueries().eq(index).realHover().find('.delete-saved-query').click(); } + static shareSavedQuery(index: number) { + this.getSavedQueries().eq(index).realHover().find('.share-saved-query').click(); + } + static getDeleteQueryConfirmation() { return cy.get('.confirmation-dialog'); } @@ -155,4 +159,24 @@ export class YasqeSteps { static rejectQueryDelete() { this.getDeleteQueryConfirmation().find('.cancel-button').click(); } + + static getShareSavedQueryDialog() { + return cy.get('.share-saved-query-dialog'); + } + + static getShareSavedQueryDialogTitle() { + return this.getShareSavedQueryDialog().find('.dialog-title'); + } + + static closeShareSavedQueryDialog() { + this.getShareSavedQueryDialog().find('.cancel-button').click(); + } + + static copySavedQueryShareLink() { + this.getShareSavedQueryDialog().find('.copy-button').click(); + } + + static getShareSavedQueryLink() { + return this.getShareSavedQueryDialog().find('.share-link-field input') + } } diff --git a/ontotext-yasgui-web-component/src/components.d.ts b/ontotext-yasgui-web-component/src/components.d.ts index aa7b815e..07408196 100644 --- a/ontotext-yasgui-web-component/src/components.d.ts +++ b/ontotext-yasgui-web-component/src/components.d.ts @@ -7,14 +7,19 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { ServiceFactory } from "./services/service-factory"; import { ConfirmationDialogConfig } from "./components/confirmation-dialog/confirmation-dialog"; +import { DialogConfig } from "./components/ontotext-dialog-web-component/ontotext-dialog-web-component"; import { ExternalYasguiConfiguration } from "./models/external-yasgui-configuration"; import { SavedQueriesData, SavedQueryConfig, SaveQueryData, UpdateQueryData } from "./models/model"; import { QueryEvent, QueryResponseEvent } from "./models/event"; +import { ShareSavedQueryDialogConfig } from "./components/share-saved-query-dialog/share-saved-query-dialog"; export namespace Components { interface ConfirmationDialog { "config": ConfirmationDialogConfig; "serviceFactory": ServiceFactory; } + interface OntotextDialogWebComponent { + "config": DialogConfig; + } /** * This is the custom web component which is adapter for the yasgui library. It allows as to * configure and extend the library without potentially breaking the component clients. @@ -56,6 +61,10 @@ export namespace Components { interface SavedQueriesPopup { "config": SavedQueriesData; } + interface ShareSavedQueryDialog { + "config": ShareSavedQueryDialogConfig; + "serviceFactory": ServiceFactory; + } interface YasguiTooltip { "dataTooltip": string; "placement": string; @@ -78,6 +87,10 @@ export interface SavedQueriesPopupCustomEvent extends CustomEvent { detail: T; target: HTMLSavedQueriesPopupElement; } +export interface ShareSavedQueryDialogCustomEvent extends CustomEvent { + detail: T; + target: HTMLShareSavedQueryDialogElement; +} declare global { interface HTMLConfirmationDialogElement extends Components.ConfirmationDialog, HTMLStencilElement { } @@ -85,6 +98,12 @@ declare global { prototype: HTMLConfirmationDialogElement; new (): HTMLConfirmationDialogElement; }; + interface HTMLOntotextDialogWebComponentElement extends Components.OntotextDialogWebComponent, HTMLStencilElement { + } + var HTMLOntotextDialogWebComponentElement: { + prototype: HTMLOntotextDialogWebComponentElement; + new (): HTMLOntotextDialogWebComponentElement; + }; /** * This is the custom web component which is adapter for the yasgui library. It allows as to * configure and extend the library without potentially breaking the component clients. @@ -119,6 +138,12 @@ declare global { prototype: HTMLSavedQueriesPopupElement; new (): HTMLSavedQueriesPopupElement; }; + interface HTMLShareSavedQueryDialogElement extends Components.ShareSavedQueryDialog, HTMLStencilElement { + } + var HTMLShareSavedQueryDialogElement: { + prototype: HTMLShareSavedQueryDialogElement; + new (): HTMLShareSavedQueryDialogElement; + }; interface HTMLYasguiTooltipElement extends Components.YasguiTooltip, HTMLStencilElement { } var HTMLYasguiTooltipElement: { @@ -127,9 +152,11 @@ declare global { }; interface HTMLElementTagNameMap { "confirmation-dialog": HTMLConfirmationDialogElement; + "ontotext-dialog-web-component": HTMLOntotextDialogWebComponentElement; "ontotext-yasgui": HTMLOntotextYasguiElement; "save-query-dialog": HTMLSaveQueryDialogElement; "saved-queries-popup": HTMLSavedQueriesPopupElement; + "share-saved-query-dialog": HTMLShareSavedQueryDialogElement; "yasgui-tooltip": HTMLYasguiTooltipElement; } } @@ -146,6 +173,9 @@ declare namespace LocalJSX { "onInternalConfirmationRejectedEvent"?: (event: ConfirmationDialogCustomEvent) => void; "serviceFactory"?: ServiceFactory; } + interface OntotextDialogWebComponent { + "config"?: DialogConfig; + } /** * This is the custom web component which is adapter for the yasgui library. It allows as to * configure and extend the library without potentially breaking the component clients. @@ -191,6 +221,14 @@ declare namespace LocalJSX { * Event emitted when after query response is returned. */ "onQueryResponse"?: (event: OntotextYasguiCustomEvent) => void; + /** + * Event emitted when saved query share link gets copied in the clipboard. + */ + "onSavedQueryShareLinkCopied"?: (event: OntotextYasguiCustomEvent) => void; + /** + * Event emitted when saved query share link has to be build by the client. + */ + "onShareSavedQuery"?: (event: OntotextYasguiCustomEvent) => void; /** * Event emitted when a query payload is updated and the query name is the same as the one being edited. In result the client must perform a query update. */ @@ -237,6 +275,22 @@ declare namespace LocalJSX { * Event fired when the delete saved query button is triggered. */ "onInternalSavedQuerySelectedForDeleteEvent"?: (event: SavedQueriesPopupCustomEvent) => void; + /** + * Event fired when the share saved query button is triggered. + */ + "onInternalSavedQuerySelectedForShareEvent"?: (event: SavedQueriesPopupCustomEvent) => void; + } + interface ShareSavedQueryDialog { + "config"?: ShareSavedQueryDialogConfig; + /** + * Internal event fired when saved query share link is copied in the clipboard. + */ + "onInternalSavedQueryShareLinkCopiedEvent"?: (event: ShareSavedQueryDialogCustomEvent) => void; + /** + * Event fired when the dialog is closed by triggering one of the close controls, e.g. close or cancel button as well as clicking outside of the dialog. + */ + "onInternalShareSavedQueryDialogClosedEvent"?: (event: ShareSavedQueryDialogCustomEvent) => void; + "serviceFactory"?: ServiceFactory; } interface YasguiTooltip { "dataTooltip"?: string; @@ -245,9 +299,11 @@ declare namespace LocalJSX { } interface IntrinsicElements { "confirmation-dialog": ConfirmationDialog; + "ontotext-dialog-web-component": OntotextDialogWebComponent; "ontotext-yasgui": OntotextYasgui; "save-query-dialog": SaveQueryDialog; "saved-queries-popup": SavedQueriesPopup; + "share-saved-query-dialog": ShareSavedQueryDialog; "yasgui-tooltip": YasguiTooltip; } } @@ -256,6 +312,7 @@ declare module "@stencil/core" { export namespace JSX { interface IntrinsicElements { "confirmation-dialog": LocalJSX.ConfirmationDialog & JSXBase.HTMLAttributes; + "ontotext-dialog-web-component": LocalJSX.OntotextDialogWebComponent & JSXBase.HTMLAttributes; /** * This is the custom web component which is adapter for the yasgui library. It allows as to * configure and extend the library without potentially breaking the component clients. @@ -275,6 +332,7 @@ declare module "@stencil/core" { "ontotext-yasgui": LocalJSX.OntotextYasgui & JSXBase.HTMLAttributes; "save-query-dialog": LocalJSX.SaveQueryDialog & JSXBase.HTMLAttributes; "saved-queries-popup": LocalJSX.SavedQueriesPopup & JSXBase.HTMLAttributes; + "share-saved-query-dialog": LocalJSX.ShareSavedQueryDialog & JSXBase.HTMLAttributes; "yasgui-tooltip": LocalJSX.YasguiTooltip & JSXBase.HTMLAttributes; } } diff --git a/ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/ontotext-dialog-web-component.scss b/ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/ontotext-dialog-web-component.scss new file mode 100644 index 00000000..b24602cf --- /dev/null +++ b/ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/ontotext-dialog-web-component.scss @@ -0,0 +1,91 @@ +@import 'src/css/common'; + +:host { + display: block; +} + +.dialog-overlay { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + // Because the tooltip we use position itself on z-index: 9999 + z-index: 9998; + margin-left: -10px; + background-color: rgba(0, 0, 0, .5); + font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif; + font-weight: 300; + color: #373a3c; +} + +.dialog { + z-index: 1001; + width: 600px; + max-width: 600px; + background-color: #fff; + box-shadow: 0 5px 15px rgb(0 0 0 / 50%); + font-family: inherit; + font-weight: inherit; + + .dialog-header { + display: flex; + justify-content: space-between; + padding: 15px; + border-bottom: 1px solid #e5e5e5; + font-size: 20px; + + .dialog-title { + margin: .5rem 0; + font-weight: 400; + } + + .close-button { + background-color: #fff; + border: none; + outline: none; + cursor: pointer; + transition: all .15s ease-out; + + &:hover, &:focus { + font-weight: bold; + } + } + } + + .dialog-body { + padding: 15px; + } + + .dialog-footer { + display: flex; + justify-content: flex-end; + padding: 15px; + border-top: 1px solid #e5e5e5; + + button { + display: inline-block; + font-weight: 400; + line-height: 1.25; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + user-select: none; + border: 1px solid transparent; + outline: none; + padding: .5rem 1rem; + font-size: 1rem; + border-radius: 0; + transition: all .15s ease-out; + } + + @include primaryButton; + @include secondaryButton; + } +} diff --git a/ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/ontotext-dialog-web-component.tsx b/ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/ontotext-dialog-web-component.tsx new file mode 100644 index 00000000..5c2648cf --- /dev/null +++ b/ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/ontotext-dialog-web-component.tsx @@ -0,0 +1,37 @@ +import {Component, h, Host, Prop} from '@stencil/core'; + +export type DialogConfig = { + dialogTitle: string; + onClose: (evt: MouseEvent) => void; +} + +@Component({ + tag: 'ontotext-dialog-web-component', + styleUrl: 'ontotext-dialog-web-component.scss', + shadow: false, +}) +export class OntotextDialogWebComponent { + + @Prop() config: DialogConfig; + + render() { + return ( + +
this.config.onClose(evt)}> +
+
+

{this.config.dialogTitle}

+ +
+
+ +
+ +
+
+
+ ); + } +} diff --git a/ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/readme.md b/ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/readme.md new file mode 100644 index 00000000..c8dc7a69 --- /dev/null +++ b/ontotext-yasgui-web-component/src/components/ontotext-dialog-web-component/readme.md @@ -0,0 +1,30 @@ +# ontotext-dialog-web-component + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------- | --------- | ----------- | -------------------------------------------------------------- | ----------- | +| `config` | -- | | `{ dialogTitle: string; onClose: (evt: MouseEvent) => void; }` | `undefined` | + + +## Dependencies + +### Used by + + - [share-saved-query-dialog](../share-saved-query-dialog) + +### Graph +```mermaid +graph TD; + share-saved-query-dialog --> ontotext-dialog-web-component + style ontotext-dialog-web-component fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/ontotext-yasgui-web-component/src/components/ontotext-yasgui-web-component/ontotext-yasgui-web-component.tsx b/ontotext-yasgui-web-component/src/components/ontotext-yasgui-web-component/ontotext-yasgui-web-component.tsx index 8c64f129..bf5b86e7 100644 --- a/ontotext-yasgui-web-component/src/components/ontotext-yasgui-web-component/ontotext-yasgui-web-component.tsx +++ b/ontotext-yasgui-web-component/src/components/ontotext-yasgui-web-component/ontotext-yasgui-web-component.tsx @@ -31,6 +31,7 @@ import { UpdateQueryData } from "../../models/model"; import {ConfirmationDialogConfig} from "../confirmation-dialog/confirmation-dialog"; +import {ShareSavedQueryDialogConfig} from "../share-saved-query-dialog/share-saved-query-dialog"; type EventArguments = [Yasqe, Request, number]; @@ -297,6 +298,49 @@ export class OntotextYasguiWebComponent { this.showSavedQueriesPopup = false; } + @State() showShareQueryDialog = false; + + /** + * Configuration for the share saved query dialog. + */ + @State() shareSavedQueryDialogConfig: ShareSavedQueryDialogConfig; + + /** + * Event emitted when saved query share link has to be build by the client. + */ + @Event() shareSavedQuery: EventEmitter; + + /** + * Event emitted when saved query share link gets copied in the clipboard. + */ + @Event() savedQueryShareLinkCopied: EventEmitter; + + /** + * Handler for the event fired when the share saved query button is triggered. + */ + @Listen('internalSavedQuerySelectedForShareEvent') + savedQuerySelectedForShareHandler(event: CustomEvent) { + this.shareSavedQuery.emit(event.detail); + this.showShareQueryDialog = true; + } + + /** + * Handler for the internal event fired when a saved query share link is copied in the clipboard. + */ + @Listen('internalSavedQueryShareLinkCopiedEvent') + savedQueryShareLinkCopiedHandler() { + this.showShareQueryDialog = false; + this.savedQueryShareLinkCopied.emit(); + } + + /** + * Handler for the event for closing the share saved query dialog. + */ + @Listen('internalShareSavedQueryDialogClosedEvent') + closeShareSavedQueryDialogHandler() { + this.showShareQueryDialog = false; + } + componentWillLoad() { // @ts-ignore if (!window.Yasgui) { @@ -452,6 +496,13 @@ export class OntotextYasguiWebComponent { } } + private getShareLinkDialogConfig(): ShareSavedQueryDialogConfig { + return { + dialogTitle: this.translationService.translate('yasqe.share.saved_query.dialog.title'), + shareQueryLink: this.savedQueryConfig.shareQueryLink + } + } + private destroy() { if (this.ontotextYasgui) { this.ontotextYasgui.destroy(); @@ -503,6 +554,10 @@ export class OntotextYasguiWebComponent { {this.showConfirmationDialog && } + + {this.showShareQueryDialog && + } ); } diff --git a/ontotext-yasgui-web-component/src/components/ontotext-yasgui-web-component/readme.md b/ontotext-yasgui-web-component/src/components/ontotext-yasgui-web-component/readme.md index 863cd86b..30c6acab 100644 --- a/ontotext-yasgui-web-component/src/components/ontotext-yasgui-web-component/readme.md +++ b/ontotext-yasgui-web-component/src/components/ontotext-yasgui-web-component/readme.md @@ -38,14 +38,16 @@ yasgui can be tweaked using the values from the configuration. ## Events -| Event | Description | Type | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -| `createSavedQuery` | Event emitted when saved query payload is collected and the query should be saved by the component client. | `CustomEvent` | -| `deleteSavedQuery` | Event emitted when a saved query should be deleted. In result the client must perform a query delete. | `CustomEvent` | -| `loadSavedQueries` | Event emitted when saved queries is expected to be loaded by the component client and provided back in order to be displayed. | `CustomEvent` | -| `queryExecuted` | Event emitted when before query to be executed. | `CustomEvent<{ query: string; }>` | -| `queryResponse` | Event emitted when after query response is returned. | `CustomEvent<{ duration: number; }>` | -| `updateSavedQuery` | Event emitted when a query payload is updated and the query name is the same as the one being edited. In result the client must perform a query update. | `CustomEvent` | +| Event | Description | Type | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| `createSavedQuery` | Event emitted when saved query payload is collected and the query should be saved by the component client. | `CustomEvent` | +| `deleteSavedQuery` | Event emitted when a saved query should be deleted. In result the client must perform a query delete. | `CustomEvent` | +| `loadSavedQueries` | Event emitted when saved queries is expected to be loaded by the component client and provided back in order to be displayed. | `CustomEvent` | +| `queryExecuted` | Event emitted when before query to be executed. | `CustomEvent<{ query: string; }>` | +| `queryResponse` | Event emitted when after query response is returned. | `CustomEvent<{ duration: number; }>` | +| `savedQueryShareLinkCopied` | Event emitted when saved query share link gets copied in the clipboard. | `CustomEvent` | +| `shareSavedQuery` | Event emitted when saved query share link has to be build by the client. | `CustomEvent` | +| `updateSavedQuery` | Event emitted when a query payload is updated and the query name is the same as the one being edited. In result the client must perform a query update. | `CustomEvent` | ## Methods @@ -69,6 +71,7 @@ Type: `Promise` - [save-query-dialog](../save-query-dialog) - [saved-queries-popup](../saved-queries-popup) - [confirmation-dialog](../confirmation-dialog) +- [share-saved-query-dialog](../share-saved-query-dialog) ### Graph ```mermaid @@ -77,7 +80,9 @@ graph TD; ontotext-yasgui --> save-query-dialog ontotext-yasgui --> saved-queries-popup ontotext-yasgui --> confirmation-dialog + ontotext-yasgui --> share-saved-query-dialog save-query-dialog --> yasgui-tooltip + share-saved-query-dialog --> ontotext-dialog-web-component style ontotext-yasgui fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/ontotext-yasgui-web-component/src/components/saved-queries-popup/readme.md b/ontotext-yasgui-web-component/src/components/saved-queries-popup/readme.md index 3cbc6dd7..c2a2cf39 100644 --- a/ontotext-yasgui-web-component/src/components/saved-queries-popup/readme.md +++ b/ontotext-yasgui-web-component/src/components/saved-queries-popup/readme.md @@ -19,6 +19,7 @@ | `internalCloseSavedQueriesPopupEvent` | Event fired when the saved queries popup should be closed. | `CustomEvent` | | `internalEditSavedQueryEvent` | Event fired when the edit saved query button is triggered. | `CustomEvent` | | `internalSavedQuerySelectedForDeleteEvent` | Event fired when the delete saved query button is triggered. | `CustomEvent` | +| `internalSavedQuerySelectedForShareEvent` | Event fired when the share saved query button is triggered. | `CustomEvent` | | `internalSaveQuerySelectedEvent` | Event fired when a saved query is selected from the list. | `CustomEvent` | diff --git a/ontotext-yasgui-web-component/src/components/saved-queries-popup/saved-queries-popup.tsx b/ontotext-yasgui-web-component/src/components/saved-queries-popup/saved-queries-popup.tsx index cf51d122..238d9b5e 100644 --- a/ontotext-yasgui-web-component/src/components/saved-queries-popup/saved-queries-popup.tsx +++ b/ontotext-yasgui-web-component/src/components/saved-queries-popup/saved-queries-popup.tsx @@ -32,13 +32,18 @@ export class SavedQueriesPopup { */ @Event() internalSavedQuerySelectedForDeleteEvent: EventEmitter; + /** + * Event fired when the share saved query button is triggered. + */ + @Event() internalSavedQuerySelectedForShareEvent: EventEmitter; + /** * Event fired when the saved queries popup should be closed. */ @Event() internalCloseSavedQueriesPopupEvent: EventEmitter; @Listen('click', {target: 'window'}) - onWindowResize(event: PointerEvent) { + onWindowResize(event: PointerEvent): void { const target: HTMLElement = event.target as HTMLElement; if (!target.closest('.saved-queries-container')) { this.internalCloseSavedQueriesPopupEvent.emit(); @@ -50,20 +55,25 @@ export class SavedQueriesPopup { this.internalSaveQuerySelectedEvent.emit(selectedQuery); } - componentDidRender() { + componentDidRender(): void { this.setPopupPosition(); } - onEdit(evt: MouseEvent, selectedQuery): void { + onEdit(evt: MouseEvent, selectedQuery: SaveQueryData): void { evt.stopPropagation(); this.internalEditSavedQueryEvent.emit(new UpdateQueryData(selectedQuery.queryName, selectedQuery.query, selectedQuery.isPublic, false)); } - onDelete(evt: MouseEvent, selectedQuery): void { + onDelete(evt: MouseEvent, selectedQuery: SaveQueryData): void { evt.stopPropagation(); this.internalSavedQuerySelectedForDeleteEvent.emit(new DeleteQueryData(selectedQuery.queryName, selectedQuery.query, selectedQuery.isPublic)); } + onShare(evt: MouseEvent, selectedQuery: SaveQueryData): void { + evt.stopPropagation(); + this.internalSavedQuerySelectedForShareEvent.emit(new DeleteQueryData(selectedQuery.queryName, selectedQuery.query, selectedQuery.isPublic)); + } + private setPopupPosition(): void { const panelRect = this.hostElement.getBoundingClientRect(); const buttonRect = this.config.popupTarget.getBoundingClientRect(); @@ -89,6 +99,9 @@ export class SavedQueriesPopup { + ))} @@ -97,5 +110,4 @@ export class SavedQueriesPopup { ); } - } diff --git a/ontotext-yasgui-web-component/src/components/share-saved-query-dialog/readme.md b/ontotext-yasgui-web-component/src/components/share-saved-query-dialog/readme.md new file mode 100644 index 00000000..c96b9b56 --- /dev/null +++ b/ontotext-yasgui-web-component/src/components/share-saved-query-dialog/readme.md @@ -0,0 +1,44 @@ +# share-saved-query-dialog + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ---------------- | --------- | ----------- | -------------------------------------------------- | ----------- | +| `config` | -- | | `{ dialogTitle: string; shareQueryLink: string; }` | `undefined` | +| `serviceFactory` | -- | | `ServiceFactory` | `undefined` | + + +## Events + +| Event | Description | Type | +| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | +| `internalSavedQueryShareLinkCopiedEvent` | Internal event fired when saved query share link is copied in the clipboard. | `CustomEvent` | +| `internalShareSavedQueryDialogClosedEvent` | Event fired when the dialog is closed by triggering one of the close controls, e.g. close or cancel button as well as clicking outside of the dialog. | `CustomEvent` | + + +## Dependencies + +### Used by + + - [ontotext-yasgui](../ontotext-yasgui-web-component) + +### Depends on + +- [ontotext-dialog-web-component](../ontotext-dialog-web-component) + +### Graph +```mermaid +graph TD; + share-saved-query-dialog --> ontotext-dialog-web-component + ontotext-yasgui --> share-saved-query-dialog + style share-saved-query-dialog fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/ontotext-yasgui-web-component/src/components/share-saved-query-dialog/share-saved-query-dialog.scss b/ontotext-yasgui-web-component/src/components/share-saved-query-dialog/share-saved-query-dialog.scss new file mode 100644 index 00000000..3f384031 --- /dev/null +++ b/ontotext-yasgui-web-component/src/components/share-saved-query-dialog/share-saved-query-dialog.scss @@ -0,0 +1,14 @@ +@import "src/css/variables"; +@import 'src/css/common'; +@import "src/css/icons"; + +:host { + display: block; +} + +.share-saved-query-dialog { + + .share-query-form { + @include formField; + } +} diff --git a/ontotext-yasgui-web-component/src/components/share-saved-query-dialog/share-saved-query-dialog.tsx b/ontotext-yasgui-web-component/src/components/share-saved-query-dialog/share-saved-query-dialog.tsx new file mode 100644 index 00000000..d195b0fa --- /dev/null +++ b/ontotext-yasgui-web-component/src/components/share-saved-query-dialog/share-saved-query-dialog.tsx @@ -0,0 +1,129 @@ +import {Component, Element, Event, EventEmitter, h, Prop} from '@stencil/core'; +import {DialogConfig} from "../ontotext-dialog-web-component/ontotext-dialog-web-component"; +import {ServiceFactory} from "../../services/service-factory"; +import {TranslationService} from "../../services/translation.service"; + +export type ShareSavedQueryDialogConfig = { + dialogTitle: string; + shareQueryLink: string; +} + +@Component({ + tag: 'share-saved-query-dialog', + styleUrl: 'share-saved-query-dialog.scss', + shadow: false, +}) +export class ShareSavedQueryDialog { + + private translationService: TranslationService; + + @Element() hostElement: HTMLElement; + + @Prop() config: ShareSavedQueryDialogConfig; + + @Prop() serviceFactory: ServiceFactory + + /** + * Event fired when the dialog is closed by triggering one of the close controls, e.g. close or + * cancel button as well as clicking outside of the dialog. + */ + @Event() internalShareSavedQueryDialogClosedEvent: EventEmitter; + + /** + * Internal event fired when saved query share link is copied in the clipboard. + */ + @Event() internalSavedQueryShareLinkCopiedEvent: EventEmitter; + + buildDialogConfig(): DialogConfig { + return { + dialogTitle: this.config.dialogTitle, + onClose: this.onClose.bind(this) + } + } + + onCopy(evt: MouseEvent): void { + evt.stopPropagation(); + this.copyToClipboard().then(() => { + this.internalSavedQueryShareLinkCopiedEvent.emit(); + }); + } + + onClose(evt: MouseEvent): void { + const target = evt.target as HTMLElement; + evt.stopPropagation(); + const isOverlay = target.classList.contains('dialog-overlay'); + const isCloseButton = target.classList.contains('close-button'); + const isCancelButton = target.classList.contains('cancel-button'); + if (isOverlay || isCloseButton || isCancelButton) { + this.internalShareSavedQueryDialogClosedEvent.emit(); + } + } + + componentWillLoad(): void { + this.translationService = this.serviceFactory.get(TranslationService); + } + + componentDidRender(): void { + const inputField: HTMLInputElement = document.querySelector('#shareLink'); + inputField.focus(); + // FIXME: For some reason this works only the first time dialog is opened + inputField.select(); + } + + private static fallbackCopyTextToClipboard(text: string): void { + const textArea = document.createElement("textarea"); + textArea.value = text; + + // Avoid scrolling to bottom + textArea.style.top = "0"; + textArea.style.left = "0"; + textArea.style.position = "fixed"; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('copy'); + } catch (err) { + console.error('Unable to copy', err); + } + + document.body.removeChild(textArea); + } + + private copyToClipboard(): Promise { + if (!navigator.clipboard) { + ShareSavedQueryDialog.fallbackCopyTextToClipboard(this.config.shareQueryLink); + return Promise.resolve(); + } + return navigator.clipboard.writeText(this.config.shareQueryLink).then(() => { + // do nothing + }, (err) => { + console.error('Could not copy share link: ', err); + }); + } + + render() { + return ( + + ); + } + +} diff --git a/ontotext-yasgui-web-component/src/css/_common.scss b/ontotext-yasgui-web-component/src/css/_common.scss index 545a7620..cc039b10 100644 --- a/ontotext-yasgui-web-component/src/css/_common.scss +++ b/ontotext-yasgui-web-component/src/css/_common.scss @@ -18,3 +18,63 @@ .hidden { display: none !important; } + +@mixin primaryButton() { + .primary-button { + background-color: $color-ontotext-orange; + border-color: $color-ontotext-orange; + color: #fff; + margin-right: 10px; + + &:hover, &:focus { + background-color: $color-ontotext-orange-dark; + border-color: $color-ontotext-orange-dark; + } + } +} + +@mixin secondaryButton() { + .secondary-button { + color: $color-onto-blue; + + &:hover, &:focus { + background-color: #d4d4d4; + border-color: #d4d4d4; + } + } +} + +@mixin formField() { + .form-field { + display: flex; + flex-direction: column; + justify-content: space-between; + + input[type=text], textarea { + padding: .5rem .75rem; + border: 1px solid rgba(0, 0, 0, .15); + font-size: 1rem; + color: #55595c; + + &:focus { + border-color: rgba(0, 54, 99, .5); + outline: none; + } + } + + input[type=text] { + + &.invalid { + border-color: #fa787e; + } + } + + textarea { + resize: vertical; + + &.invalid { + border-color: #fa787e; + } + } + } +} diff --git a/ontotext-yasgui-web-component/src/i18n/locale-en.json b/ontotext-yasgui-web-component/src/i18n/locale-en.json index 62388b50..d649a13b 100644 --- a/ontotext-yasgui-web-component/src/i18n/locale-en.json +++ b/ontotext-yasgui-web-component/src/i18n/locale-en.json @@ -85,5 +85,7 @@ "yasqe.actions.edit_saved_query.button.tooltip": "Edit", "yasqe.actions.delete_saved_query.button.tooltip": "Delete", "yasqe.actions.delete_saved_query.confirm.dialog.label": "Confirm", - "yasqe.actions.delete_saved_query.confirm.dialog.message": "Are you sure you want to delete the saved query '{{query}}'?" + "yasqe.actions.delete_saved_query.confirm.dialog.message": "Are you sure you want to delete the saved query '{{query}}'?", + "yasqe.share.saved_query.dialog.title": "Copy URL to clipboard", + "yasqe.share.saved_query.dialog.copy.btn.label": "Copy to clipboard" } diff --git a/ontotext-yasgui-web-component/src/i18n/locale-fr.json b/ontotext-yasgui-web-component/src/i18n/locale-fr.json index f2da530f..c95b8c4a 100644 --- a/ontotext-yasgui-web-component/src/i18n/locale-fr.json +++ b/ontotext-yasgui-web-component/src/i18n/locale-fr.json @@ -86,5 +86,7 @@ "yasqe.actions.edit_saved_query.button.tooltip": "Edit", "yasqe.actions.delete_saved_query.button.tooltip": "Delete", "yasqe.actions.delete_saved_query.confirm.dialog.label": "Confirm", - "yasqe.actions.delete_saved_query.confirm.dialog.message": "Are you sure you want to delete the saved query '{{query}}'?" + "yasqe.actions.delete_saved_query.confirm.dialog.message": "Are you sure you want to delete the saved query '{{query}}'?", + "yasqe.share.saved_query.dialog.title": "Copy URL to clipboard", + "yasqe.share.saved_query.dialog.copy.btn.label": "Copy to clipboard" } diff --git a/ontotext-yasgui-web-component/src/models/model.ts b/ontotext-yasgui-web-component/src/models/model.ts index 6164884c..4853d039 100644 --- a/ontotext-yasgui-web-component/src/models/model.ts +++ b/ontotext-yasgui-web-component/src/models/model.ts @@ -16,7 +16,14 @@ export interface SavedQueryConfig { * inside it. */ saveSuccess: boolean; + /** + * Any error message which can appear during the query saving. + */ errorMessage: string[]; + /** + * A link for sharing particular saved query. + */ + shareQueryLink?: string; } export interface SavedQueryInput { diff --git a/ontotext-yasgui-web-component/src/pages/actions/main.js b/ontotext-yasgui-web-component/src/pages/actions/main.js index 919b49fc..0bc8a54b 100644 --- a/ontotext-yasgui-web-component/src/pages/actions/main.js +++ b/ontotext-yasgui-web-component/src/pages/actions/main.js @@ -117,6 +117,26 @@ ontoElement.addEventListener('deleteSavedQuery', (event) => { }; }); +ontoElement.addEventListener('shareSavedQuery', (event) => { + let selectedQuery = event.detail; + const url = [location.protocol, '//', location.host, location.pathname]; + if (selectedQuery.queryName) { + url.push(...['?savedQueryName=', encodeURIComponent(selectedQuery.queryName)]); + } + if (selectedQuery.owner) { + url.push(...['&owner=', encodeURIComponent(selectedQuery.owner)]); + } + ontoElement.savedQueryConfig = { + shareQueryLink: url.join('') + }; +}); + +ontoElement.addEventListener('savedQueryShareLinkCopied', () => { + navigator.clipboard.readText().then((clipText) => { + textAreaElement.value = clipText; + }); +}); + let savedQueries = [ { "queryName": "Add statements",