forked from MapComplete/MapComplete
		
	UX: add unlink button, simplify unlink code
This commit is contained in:
		
							parent
							
								
									45c0f1a8d6
								
							
						
					
					
						commit
						1192434b45
					
				
					 13 changed files with 117 additions and 69 deletions
				
			
		|  | @ -34,6 +34,9 @@ export default class GenericImageProvider extends ImageProvider { | |||
|                 provider: this, | ||||
|                 id: value, | ||||
|                 isSpherical: undefined, | ||||
|                 originalAttribute: { | ||||
|                     key, value | ||||
|                 } | ||||
|             }, | ||||
|         ] | ||||
|     } | ||||
|  |  | |||
|  | @ -26,6 +26,7 @@ export interface ProvidedImage { | |||
|     host?: string | ||||
|     isSpherical: boolean | ||||
|     license?: LicenseInfo | ||||
|     originalAttribute?: {key: string, value: string} | ||||
| } | ||||
| 
 | ||||
| export interface PanoramaView { | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ export class Imgur extends ImageProvider { | |||
|                     provider: this, | ||||
|                     id: value, | ||||
|                     isSpherical: false, | ||||
|                     originalAttribute: {key, value} | ||||
|                 }, | ||||
|             ] | ||||
|         } | ||||
|  |  | |||
|  | @ -170,8 +170,7 @@ export class Mapillary extends ImageProvider { | |||
|             properties: { | ||||
|                 url: response.thumb_2048_url, | ||||
|                 northOffset: response.computed_compass_angle, | ||||
|                 provider: this, | ||||
|                 imageMeta: <any>image | ||||
|                 provider: this | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
|  | @ -246,6 +245,7 @@ export class Mapillary extends ImageProvider { | |||
|                 response.camera_type === "spherical" || response.camera_type === "equirectangular", | ||||
|             lat: geometry.coordinates[1], | ||||
|             lon: geometry.coordinates[0], | ||||
|             originalAttribute: {key, value} | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -174,6 +174,7 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|         } | ||||
|         const providedImage = await this.getInfo(value) | ||||
|         providedImage.alt_id = alt_id | ||||
|         providedImage.originalAttribute = {key, value} | ||||
|         return [providedImage] | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -61,7 +61,11 @@ export class WikidataImageProvider extends ImageProvider { | |||
|             allImages.push(promises) | ||||
|         } | ||||
|         const resolved = await Promise.all(Utils.NoNull(allImages)) | ||||
|         return [].concat(...resolved) | ||||
|         const flattened = resolved.flatMap( x => x) | ||||
|         if(flattened.length === 1){ | ||||
|             flattened[0].originalAttribute = {key, value} | ||||
|         } | ||||
|         return flattened | ||||
|     } | ||||
| 
 | ||||
|     public DownloadAttribution(): Promise<undefined> { | ||||
|  |  | |||
|  | @ -145,14 +145,14 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|                 .map((image) => this.UrlForImage(image)) | ||||
|         } | ||||
|         if (value.startsWith("File:")) { | ||||
|             return [this.UrlForImage(value)] | ||||
|             return [this.UrlForImage(value, key, value)] | ||||
|         } | ||||
|         if (value.startsWith("http")) { | ||||
|             // Probably an error
 | ||||
|             return undefined | ||||
|         } | ||||
|         // We do a last effort and assume this is a file
 | ||||
|         return [this.UrlForImage("File:" + value)] | ||||
|         return [this.UrlForImage("File:" + value, key, value)] | ||||
|     } | ||||
| 
 | ||||
|     public async DownloadAttribution(img: { id: string }): Promise<LicenseInfo> { | ||||
|  | @ -211,9 +211,9 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|         return licenseInfo | ||||
|     } | ||||
| 
 | ||||
|     private UrlForImage(image: string): ProvidedImage { | ||||
|     private UrlForImage(image: string, key?: string, value?: string): ProvidedImage { | ||||
|         image = "File:" + WikimediaImageProvider.makeCanonical(image) | ||||
|         return { | ||||
|         const providedImage: ProvidedImage = { | ||||
|             url: WikimediaImageProvider.PrepareUrl(image), | ||||
|             url_hd: WikimediaImageProvider.PrepareUrl(image, true), | ||||
|             key: undefined, | ||||
|  | @ -221,6 +221,10 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|             id: image, | ||||
|             isSpherical: false, | ||||
|         } | ||||
|         if(key && value){ | ||||
|             providedImage.originalAttribute = {key, value} | ||||
|         } | ||||
|         return providedImage | ||||
|     } | ||||
| 
 | ||||
|     getPanoramaInfo(): Promise<Feature<Point, PanoramaView>> | undefined { | ||||
|  |  | |||
|  | @ -200,7 +200,6 @@ export default class UserRelatedState { | |||
|     public static readonly usersettingsConfig = UserRelatedState.initUserSettingsState() | ||||
|     public static readonly availableUserSettingsIds: string[] = | ||||
|         UserRelatedState.usersettingsConfig?.tagRenderings?.map((tr) => tr.id) ?? [] | ||||
|     public static readonly SHOW_TAGS_VALUES = ["always", "yes", "full"] as const | ||||
|     /** | ||||
|      The user credentials | ||||
|      */ | ||||
|  | @ -212,6 +211,7 @@ export default class UserRelatedState { | |||
|     public readonly installedUserThemes: Store<string[]> | ||||
|     public readonly showAllQuestionsAtOnce: UIEventSource<boolean> | ||||
|     public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> | ||||
|     public readonly showTagsB: Store<boolean> | ||||
|     public readonly showCrosshair: UIEventSource<"yes" | "always" | "no" | undefined> | ||||
|     public readonly translationMode: UIEventSource<"false" | "true" | "mobile" | undefined | string> | ||||
| 
 | ||||
|  | @ -269,6 +269,20 @@ export default class UserRelatedState { | |||
|         ) | ||||
|         this.language = this.osmConnection.getPreference("language") | ||||
|         this.showTags = this.osmConnection.getPreference("show_tags") | ||||
|         this.showTagsB = this.showTags.map(showTags => { | ||||
|             if (showTags === "always" || showTags === "full") { | ||||
|                 return true | ||||
|             } | ||||
|             if (showTags === "no") { | ||||
|                 return false | ||||
|             } | ||||
|             const userdetails = this.osmConnection.userDetails.data | ||||
|             if (!userdetails) { | ||||
|                 return false | ||||
|             } | ||||
|             const csCount = userdetails.csCount | ||||
|             return csCount >= Constants.userJourney.tagsVisibleAt | ||||
|         }, [this.osmConnection.userDetails]) | ||||
|         this.showCrosshair = this.osmConnection.getPreference("show_crosshair") | ||||
|         this.fixateNorth = this.osmConnection.getPreference("fixate-north") | ||||
|         this.morePrivacy = this.osmConnection.getPreference("more_privacy", { defaultValue: "no" }) | ||||
|  |  | |||
|  | @ -22,7 +22,8 @@ | |||
|    */ | ||||
|   export let silentFail: boolean = false | ||||
|   /** | ||||
|    * If set and the OSM-api  fails, do _not_ show any error messages nor the successful state, just hide | ||||
|    * If set and the OSM-api fails, do _not_ show any error messages nor the successful state, just hide. | ||||
|    * Will still show the "not-logged-in"-slot | ||||
|    */ | ||||
|   export let hiddenFail: boolean = false | ||||
|   let loadingStatus = state?.osmConnection?.loadingStatus ?? new ImmutableStore("logged-in") | ||||
|  |  | |||
|  | @ -24,14 +24,7 @@ | |||
|   let isDisplayed: UIEventSource<boolean> = filteredLayer.isDisplayed | ||||
| 
 | ||||
|   let isDebugging = state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false) | ||||
|   let showTags = state?.userRelatedState?.showTags?.map( | ||||
|     (s) => | ||||
|       (s === "yes" && | ||||
|         state?.userRelatedState?.osmConnection?.userDetails?.data?.csCount >= | ||||
|           Constants.userJourney.tagsVisibleAt) || | ||||
|       s === "always" || | ||||
|       s === "full" | ||||
|   ) | ||||
|   let showTags = state?.userRelatedState?.showTagsB | ||||
| 
 | ||||
|   /** | ||||
|    * Gets a UIEventSource as boolean for the given option, to be used with a checkbox | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ | |||
|   import Panorama360 from "../../assets/svg/Panorama360.svelte" | ||||
|   import { ExternalLinkIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { ExclamationTriangle as TriangleOutline } from "@babeard/svelte-heroicons/outline/ExclamationTriangle" | ||||
|   import LoginToggle from "../Base/LoginToggle.svelte" | ||||
| 
 | ||||
|   export let image: Partial<ProvidedImage> & { id: string; url: string } | ||||
|   let fallbackImage: string = undefined | ||||
|  | @ -43,16 +44,39 @@ | |||
| 
 | ||||
|   let loaded = false | ||||
|   let error = false | ||||
|   let notFound = false | ||||
|   let ignoreHidden = false | ||||
|   let isInStrictMode = new UIEventSource(false) | ||||
| 
 | ||||
|   function onError() { | ||||
|     error = true | ||||
|   async function detectErrorReason() { | ||||
|     try { | ||||
| 
 | ||||
|       const response = await fetch( | ||||
|         image.url, | ||||
|         { | ||||
|           headers: { | ||||
|             "Accept": "image/avif,image/webp,*/*", | ||||
|           }, | ||||
|         }, | ||||
|       ) | ||||
|       if (response.status === 404) { | ||||
|         notFound = true | ||||
|       } | ||||
|     } catch | ||||
|       (e) { | ||||
|       console.log("Could not load image while trying to remediate", e) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async function onError() { | ||||
|     Mapillary.isInStrictMode().addCallbackAndRunD(isStrict => { | ||||
|       isInStrictMode.set(isStrict) | ||||
|       return true // unregister | ||||
|     }) | ||||
|     await detectErrorReason() | ||||
|     error = true | ||||
|   } | ||||
| 
 | ||||
|   let visitUrl = image.provider?.visitUrl(image) | ||||
|   let showBigPreview = new UIEventSource(false) | ||||
|   onDestroy( | ||||
|  | @ -112,15 +136,25 @@ | |||
| </Popup> | ||||
| {#if error} | ||||
|   <div class="h-80 w-60 interactive flex flex-col justify-center items-center p-4 text-center"> | ||||
|     <div class="alert flex items-center"> | ||||
|       <TriangleOutline class="shrink-0 h-8 w-8" /> | ||||
|       <Tr t={Translations.t.image.loadingFailed}/> | ||||
|     </div> | ||||
|     {#if image.provider.name.toLowerCase() === "mapillary" && $isInStrictMode} | ||||
|       <Tr t={Translations.t.image.mapillaryTrackingProtection}/> | ||||
|     {:else if $isInStrictMode} | ||||
|       <Tr t={Translations.t.image.strictProtectionDetected}/> | ||||
|       <div class="subtle text-sm mt-8">{image.url}</div> | ||||
|     {#if notFound} | ||||
|       <div class="alert flex items-center"> | ||||
|         <TriangleOutline class="shrink-0 h-8 w-8" /> | ||||
|         Not found | ||||
|       </div> | ||||
|       This image is probably incorrect or deleted. | ||||
|       <slot name="not-found-extra" /> | ||||
|     {:else} | ||||
|       <div class="alert flex items-center"> | ||||
|         <TriangleOutline class="shrink-0 h-8 w-8" /> | ||||
|         <Tr t={Translations.t.image.loadingFailed} /> | ||||
|       </div> | ||||
|       {#if image.provider.name.toLowerCase() === "mapillary" && $isInStrictMode} | ||||
|         <Tr t={Translations.t.image.mapillaryTrackingProtection} /> | ||||
|       {:else if $isInStrictMode} | ||||
|         <Tr t={Translations.t.image.strictProtectionDetected} /> | ||||
|         {image.provider.name} | ||||
|         <div class="subtle text-sm mt-8">{image.url}</div> | ||||
|       {/if} | ||||
|     {/if} | ||||
|   </div> | ||||
| {:else if image.status !== undefined && image.status !== "ready" && image.status !== "hidden"} | ||||
|  |  | |||
|  | @ -39,6 +39,8 @@ | |||
|   let reportFreeText = new UIEventSource<string>(undefined) | ||||
|   let reported = new UIEventSource<boolean>(false) | ||||
| 
 | ||||
|   let canBeUnlinked = image.originalAttribute !== undefined | ||||
| 
 | ||||
|   async function requestDeletion() { | ||||
|     if (reportReason.data === "other" && !reportFreeText.data) { | ||||
|       return | ||||
|  | @ -63,31 +65,20 @@ | |||
|   } | ||||
| 
 | ||||
|   async function unlink() { | ||||
|     console.log("Unlinking image", image.key, image.id) | ||||
|     if (image.id.length < 10) { | ||||
|       console.error("Suspicious value, not deleting ", image.id) | ||||
|       return | ||||
|     } | ||||
|     // The "key" is the provider key, but not necessarely the actual key that should be reset | ||||
|     // We iterate over all tags. *Every* tag for which the value contains the id will be deleted | ||||
|     const tgs = tags.data | ||||
|     for (const key in tgs) { | ||||
|       if (typeof tgs[key] !== "string" || tgs[key].indexOf(image.id) < 0) { | ||||
|         continue | ||||
|       } | ||||
| 
 | ||||
|       await state?.changes?.applyAction( | ||||
|         new ChangeTagAction(tgs.id, new Tag(key, ""), tgs, { | ||||
|           changeType: "delete-image", | ||||
|           theme: state.theme.id, | ||||
|         }) | ||||
|       ) | ||||
|     } | ||||
|     const {key} = image.originalAttribute | ||||
|     await state?.changes?.applyAction( | ||||
|       new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data, { | ||||
|         changeType: "delete-image", | ||||
|         theme: state.theme.id, | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
| 
 | ||||
|   const t = Translations.t.image.panoramax | ||||
|   const tu = Translations.t.image.unlink | ||||
|   const placeholder = t.placeholder.current | ||||
| 
 | ||||
|   let showTags = state.userRelatedState?.showTagsB | ||||
| </script> | ||||
| 
 | ||||
| <Popup shown={showDeleteDialog}> | ||||
|  | @ -169,10 +160,24 @@ | |||
|           <DownloadIcon /> | ||||
|           <Tr t={Translations.t.general.download.downloadImage} /> | ||||
|         </button> | ||||
|         <button on:click={() => showDeleteDialog.set(true)} class="flex items-center"> | ||||
|           <TrashIcon /> | ||||
|           <Tr t={tu.button} /> | ||||
|         </button> | ||||
|         {#if canBeUnlinked} | ||||
|           <button on:click={() => showDeleteDialog.set(true)} class="flex items-center"> | ||||
|             <TrashIcon /> | ||||
|             <Tr t={tu.button} /> | ||||
|           </button> | ||||
|         {/if} | ||||
|       </svelte:fragment> | ||||
|       <svelte:fragment slot="not-found-extra"> | ||||
|         {#if canBeUnlinked} | ||||
|           <button on:click={() => unlink()}> | ||||
|             <Tr t={tu.button} /> | ||||
|           </button> | ||||
|           {#if $showTags} | ||||
|             <div class="subtle line-through"> | ||||
|               {image.originalAttribute.key}={image.originalAttribute.value} | ||||
|             </div> | ||||
|           {/if} | ||||
|         {/if} | ||||
|       </svelte:fragment> | ||||
|     </AttributedImage> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -16,9 +16,7 @@ | |||
|   import SubtleButton from "../../Base/SubtleButton.svelte" | ||||
|   import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte" | ||||
|   import { Translation } from "../../i18n/Translation" | ||||
|   import Constants from "../../../Models/Constants" | ||||
|   import { Unit } from "../../../Models/Unit" | ||||
|   import UserRelatedState from "../../../Logic/State/UserRelatedState" | ||||
|   import { twJoin } from "tailwind-merge" | ||||
|   import { TagUtils } from "../../../Logic/Tags/TagUtils" | ||||
| 
 | ||||
|  | @ -31,8 +29,8 @@ | |||
|   import { get } from "svelte/store" | ||||
|   import Markdown from "../../Base/Markdown.svelte" | ||||
|   import { Utils } from "../../../Utils" | ||||
|   import { TagTypes } from "../../../Logic/Tags/TagTypes" | ||||
|   import type { UploadableTag } from "../../../Logic/Tags/TagTypes" | ||||
|   import { TagTypes } from "../../../Logic/Tags/TagTypes" | ||||
| 
 | ||||
|   import Popup from "../../Base/Popup.svelte" | ||||
|   import If from "../../Base/If.svelte" | ||||
|  | @ -315,8 +313,7 @@ | |||
|   let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false) | ||||
|   let featureSwitchIsDebugging = | ||||
|     state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false) | ||||
|   let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined) | ||||
|   let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0 | ||||
|   let showTags : Store<boolean> = state?.userRelatedState?.showTagsB ?? new ImmutableStore(false) | ||||
|   let question = config.question | ||||
|   let hideMappingsUnlessSearchedFor = | ||||
|     config.mappings.length > 8 && config.mappings.some((m) => m.priorityIf !== undefined) | ||||
|  | @ -324,14 +321,6 @@ | |||
|   $: hideMappingsUnlessSearchedFor = | ||||
|     config.mappings.length > 8 && config.mappings.some((m) => m.priorityIf !== undefined) | ||||
| 
 | ||||
|   if (state?.osmConnection) { | ||||
|     onDestroy( | ||||
|       state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { | ||||
|         numberOfCs = ud?.csCount | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
| 
 | ||||
|   function clearAnswer() { | ||||
|     const tagsToSet: UploadableTag[] = onMarkUnknown.data | ||||
|     const change = new ChangeTagAction(tags.data.id, new And(tagsToSet), tags.data, { | ||||
|  | @ -577,9 +566,7 @@ | |||
|             </h2> | ||||
|             <Tr t={Translations.t.unknown.explanation} /> | ||||
|             <If | ||||
|               condition={state.userRelatedState?.showTags?.map( | ||||
|                 (v) => v === "yes" || v === "full" || v === "always" | ||||
|               )} | ||||
|               condition={state.userRelatedState?.showTagsB} | ||||
|             > | ||||
|               <div class="subtle"> | ||||
|                 <Tr t={Translations.t.unknown.removedKeys} /> | ||||
|  | @ -639,7 +626,7 @@ | |||
|             </div> | ||||
|           </div> | ||||
|           <!-- Taghint + debug info --> | ||||
|           {#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging} | ||||
|           {#if $showTags || $featureSwitchIsTesting || $featureSwitchIsDebugging} | ||||
|             <span class="flex flex-wrap justify-between"> | ||||
|               <TagHint tags={selectedTags} currentProperties={$tags} /> | ||||
|               <span class="flex flex-wrap"> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue